classifyUpstreamError maps a raw network error into the (statusCode, reasonPhrase, marker-line) tuple used by synthesizeUpstreamErrorResponse. - err == nil (unexpected, but handled) -> 502 Bad Gateway (generic marker) - context.DeadlineExceeded / net.Error.Timeout() -> 504 Gateway Timeout
(err error)
| 43 | // line is inlined into the body AND emitted as a response header so |
| 44 | // downstream diff tooling (and humans) can grep for it in recorded YAML. |
| 45 | func classifyUpstreamError(err error) (status int, reason, marker string) { |
| 46 | if err == nil { |
| 47 | // Don't classify nil as a timeout — that would mislabel any |
| 48 | // synthesized response whose caller forgot to pass the underlying |
| 49 | // error, and poison downstream timeout analysis. Use the generic |
| 50 | // 502 marker instead. |
| 51 | return 502, "Bad Gateway", UpstreamGenericMarker |
| 52 | } |
| 53 | |
| 54 | // Timeouts: DeadlineExceeded or any net.Error whose Timeout() flag is set. |
| 55 | if errors.Is(err, context.DeadlineExceeded) { |
| 56 | return 504, "Gateway Timeout", UpstreamTimeoutMarker |
| 57 | } |
| 58 | var netErr net.Error |
| 59 | if errors.As(err, &netErr) && netErr.Timeout() { |
| 60 | return 504, "Gateway Timeout", UpstreamTimeoutMarker |
| 61 | } |
| 62 | |
| 63 | // Mid-stream / early EOF: upstream closed the socket before sending a |
| 64 | // response (or mid-body). This is "bad gateway" territory rather than a |
| 65 | // timeout — but it is still something the recorder must persist rather |
| 66 | // than drop, otherwise replay cannot reproduce the observed error. |
| 67 | if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { |
| 68 | return 502, "Bad Gateway", "keploy-recorded-upstream-eof: true" |
| 69 | } |
| 70 | |
| 71 | // Connection-refused / reset / host-unreachable fall into a generic 502 |
| 72 | // with a more specific marker so post-hoc analysis can pivot by error |
| 73 | // class. |
| 74 | msg := strings.ToLower(err.Error()) |
| 75 | if strings.Contains(msg, "connection refused") || |
| 76 | strings.Contains(msg, "connection reset") || |
| 77 | strings.Contains(msg, "no route to host") || |
| 78 | strings.Contains(msg, "broken pipe") { |
| 79 | return 502, "Bad Gateway", "keploy-recorded-upstream-unreachable: true" |
| 80 | } |
| 81 | |
| 82 | // Everything else still gets persisted — default to 502 so replay sees a |
| 83 | // deterministic error instead of silently dropping. |
| 84 | return 502, "Bad Gateway", "keploy-recorded-upstream-error: true" |
| 85 | } |
| 86 | |
| 87 | // upstreamErrorClass returns a short human label for an upstream error — |
| 88 | // "timeout", "eof", "unreachable", or "other" — suitable for structured |