Echo JSON Through Frontend
Waiting for request...
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 = `
Waiting for request...