package main // This is a runable example which will spawn two servers, one we can access // which hits the other and response with the data provided. import ( "bytes" "encoding/json" "fmt" "io" "net" "net/http" "net/url" "os" "strconv" "strings" "sync" "time" ) func main() { upstreamHost, err := findNonLoopbackIPv4() if err != nil { fmt.Printf("error: %v\n", err) os.Exit(1) } var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() if err := startUpstream(upstreamHost); err != nil { fmt.Printf("error: %v\n", err) os.Exit(1) } }() go func() { defer wg.Done() if err := startFrontend(upstreamHost); err != nil { fmt.Printf("error: %v\n", err) os.Exit(1) } }() wg.Wait() } func startFrontend(upstreamHost string) error { mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = w.Write([]byte(frontendHTML)) }) mux.HandleFunc("/echo", func(w http.ResponseWriter, req *http.Request) { client := &http.Client{Timeout: parseTimeout(req.URL.Query().Get("timeoutMs"))} codeParam := strings.TrimSpace(req.URL.Query().Get("code")) failParam := strings.TrimSpace(req.URL.Query().Get("fail")) if failParam != "" && failParam != "false" && failParam != "0" && failParam != "no" { fmt.Fprintf(os.Stderr, "frontend fail mode requested method=%s fail=%s\n", req.Method, failParam) } if parsedCode := parseStatusCode(codeParam); parsedCode >= 400 { fmt.Fprintf(os.Stderr, "frontend error status requested method=%s code=%d fail=%s\n", req.Method, parsedCode, failParam) } switch req.Method { case http.MethodGet: message := req.URL.Query().Get("message") upstreamURL := fmt.Sprintf( "http://%s:3001/echo?message=%s&code=%s&fail=%s&sleepMs=%s", upstreamHost, url.QueryEscape(message), url.QueryEscape(codeParam), url.QueryEscape(failParam), url.QueryEscape(req.URL.Query().Get("sleepMs")), ) fmt.Printf("frontend -> upstream GET %s\n", upstreamURL) resp, err := client.Get(upstreamURL) if err != nil { fmt.Fprintf(os.Stderr, "frontend upstream GET failed url=%s err=%v\n", upstreamURL, err) http.Error(w, err.Error(), http.StatusBadGateway) return } defer resp.Body.Close() if resp.StatusCode >= 400 { fmt.Fprintf(os.Stderr, "frontend upstream responded with error method=GET status=%d url=%s\n", resp.StatusCode, upstreamURL) } body, err := io.ReadAll(resp.Body) if err != nil { http.Error(w, err.Error(), http.StatusBadGateway) return } w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(resp.StatusCode) _, _ = w.Write(body) case http.MethodPost: body, err := io.ReadAll(req.Body) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if !json.Valid(body) { http.Error(w, "invalid JSON payload", http.StatusBadRequest) return } upstreamURL := fmt.Sprintf( "http://%s:3001/echo?code=%s&fail=%s&sleepMs=%s", upstreamHost, url.QueryEscape(codeParam), url.QueryEscape(failParam), url.QueryEscape(req.URL.Query().Get("sleepMs")), ) fmt.Printf("frontend -> upstream POST %s\n", upstreamURL) upstreamReq, err := http.NewRequest(http.MethodPost, upstreamURL, bytes.NewReader(body)) if err != nil { http.Error(w, err.Error(), http.StatusBadGateway) return } upstreamReq.Header.Set("Content-Type", "application/json") resp, err := client.Do(upstreamReq) if err != nil { fmt.Fprintf(os.Stderr, "frontend upstream POST failed url=%s err=%v\n", upstreamURL, err) http.Error(w, err.Error(), http.StatusBadGateway) return } defer resp.Body.Close() if resp.StatusCode >= 400 { fmt.Fprintf(os.Stderr, "frontend upstream responded with error method=POST status=%d url=%s\n", resp.StatusCode, upstreamURL) } upstreamBody, err := io.ReadAll(resp.Body) if err != nil { http.Error(w, err.Error(), http.StatusBadGateway) return } w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(resp.StatusCode) _, _ = w.Write(upstreamBody) default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } }) fmt.Println("frontend UI on http://127.0.0.1:3000") fmt.Println("frontend GET example: http://127.0.0.1:3000/echo?message=hello&code=201&sleepMs=200") fmt.Println("frontend POST example: curl -i -X POST 'http://127.0.0.1:3000/echo?code=202&sleepMs=200' -H 'content-type: application/json' -d '{\"message\":\"hello\"}'") fmt.Println("frontend timeout example: http://127.0.0.1:3000/echo?message=late&sleepMs=4000&timeoutMs=1000") fmt.Println("frontend failure examples: fail=true, fail=drop, fail=timeout, fail=status") fmt.Printf("frontend calls upstream at http://%s:3001/echo\n", upstreamHost) return http.ListenAndServe("127.0.0.1:3000", mux) } func startUpstream(upstreamHost string) error { mux := http.NewServeMux() mux.HandleFunc("/echo", func(w http.ResponseWriter, req *http.Request) { code := parseStatusCode(req.URL.Query().Get("code")) time.Sleep(parseSleep(req.URL.Query().Get("sleepMs"))) if handleFailureMode(w, req, req.URL.Query().Get("fail"), code) { return } switch req.Method { case http.MethodGet: message := req.URL.Query().Get("message") w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(code) _, _ = w.Write([]byte(message)) case http.MethodPost: body, err := io.ReadAll(req.Body) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(code) _, _ = w.Write(body) default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } }) fmt.Printf("upstream listening on http://%s:3001/echo?message=hello&code=201\n", upstreamHost) fmt.Printf("upstream POST example: curl -i -X POST 'http://%s:3001/echo?code=202&sleepMs=200' -H 'content-type: application/json' -d '{\"message\":\"hello\"}'\n", upstreamHost) return http.ListenAndServe(":3001", mux) } const frontendHTML = ` Echo JSON Demo

Echo JSON Through Frontend

Waiting for request...
` func handleFailureMode(w http.ResponseWriter, req *http.Request, raw string, requestedCode int) bool { mode := strings.ToLower(strings.TrimSpace(raw)) if mode == "" || mode == "false" || mode == "0" || mode == "no" { return false } switch mode { case "true", "drop": hj, ok := w.(http.Hijacker) if !ok { http.Error(w, "drop mode not supported by server", http.StatusInternalServerError) return true } conn, _, err := hj.Hijack() if err != nil { http.Error(w, "failed to drop connection", http.StatusInternalServerError) return true } _ = conn.Close() return true case "timeout", "hang": <-req.Context().Done() return true case "status": status := requestedCode if status < 400 || status > 599 { status = http.StatusInternalServerError } http.Error(w, fmt.Sprintf("forced failure (%d)", status), status) return true default: if status, ok := parseFailureStatus(mode); ok { http.Error(w, fmt.Sprintf("forced failure (%d)", status), status) return true } http.Error(w, "invalid fail mode", http.StatusBadRequest) return true } } func parseFailureStatus(mode string) (int, bool) { status, err := strconv.Atoi(mode) if err != nil || status < 400 || status > 599 { return 0, false } return status, true } func parseStatusCode(raw string) int { if raw == "" { return http.StatusOK } code, err := strconv.Atoi(raw) if err != nil || code < 100 || code > 999 { return http.StatusOK } return code } func parseSleep(raw string) time.Duration { ms, ok := parseMilliseconds(raw, 0) if !ok { return 0 } return time.Duration(ms) * time.Millisecond } func parseTimeout(raw string) time.Duration { ms, ok := parseMilliseconds(raw, 5000) if !ok { return 5 * time.Second } if ms == 0 { return 0 } return time.Duration(ms) * time.Millisecond } func parseMilliseconds(raw string, fallback int) (int, bool) { if raw == "" { return fallback, true } ms, err := strconv.Atoi(raw) if err != nil || ms < 0 { return fallback, false } return ms, true } func findNonLoopbackIPv4() (string, error) { addrs, err := net.InterfaceAddrs() if err != nil { return "", err } for _, addr := range addrs { ipNet, ok := addr.(*net.IPNet) if !ok || ipNet.IP == nil { continue } ip := ipNet.IP.To4() if ip == nil || ip.IsLoopback() { continue } return ip.String(), nil } return "", fmt.Errorf("no non-loopback IPv4 address found; this demo needs one so outbound traffic does not bypass the proxy") }