@@ -134,15 +134,15 @@ func TestNewForwarder_DisableKeepAlives_OpensNewConnectionPerRequest(t *testing.
134134 "DisableKeepAlives must open a fresh TCP connection per request; with keep-alives enabled only 1 connection would be accepted" )
135135}
136136
137- // NewForwarder defaults reflect the configured wire.go constants. /ready keeps
138- // a 60s budget (matches the platform hook timeout); the remaining hooks default
139- // to 1s. Changing these defaults is a deliberate behavior change — the test
140- // failure is intentional.
137+ // NewForwarder defaults reflect the configured wire.go constants. /ready and
138+ // /validate each keep a 30s budget, and runtime hooks default to 1s. Changing
139+ // these defaults is a deliberate behavior change — the test failure is
140+ // intentional.
141141func TestNewForwarder_Defaults (t * testing.T ) {
142142 f := NewForwarder (8080 , defaultForwardTimeout , defaultReadyTimeout , defaultValidateTimeout )
143143 assert .Equal (t , 1 * time .Second , f .forwardTimeout , "forwardTimeout default must be 1s" )
144- assert .Equal (t , 60 * time .Second , f .readyTimeout , "readyTimeout default must be 60s (matches platform /ready hook timeout)" )
145- assert .Equal (t , 1 * time .Second , f .validateTimeout , "validateTimeout default must be 1s " )
144+ assert .Equal (t , 30 * time .Second , f .readyTimeout , "readyTimeout default must be 30s (matches platform /ready hook timeout)" )
145+ assert .Equal (t , 30 * time .Second , f .validateTimeout , "validateTimeout default must be 30s " )
146146 assert .Equal (t , defaultMaxResponseBodyBytes , f .maxResponseBodyBytes , "maxResponseBodyBytes default must be 1 MiB" )
147147 tr , ok := f .client .Transport .(* http.Transport )
148148 require .True (t , ok , "transport must be *http.Transport" )
@@ -266,7 +266,8 @@ func TestForwarder_PassThroughWaiting_MirrorsStatusAndBody(t *testing.T) {
266266// PassThroughWaiting must honor the timeout it receives. The caller (server.go)
267267// passes readyTimeout or validateTimeout — PassThroughWaiting must not apply
268268// any other field. This test uses a 50ms timeout against a 200ms upstream: the
269- // context expires and the call returns 504.
269+ // context expires and the call returns 503 (per the /ready and /validate hook
270+ // contract, only 200 and 503 are meaningful — never 504).
270271func TestForwarder_PassThroughWaiting_HonorsProvidedTimeout (t * testing.T ) {
271272 srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , _ * http.Request ) {
272273 time .Sleep (200 * time .Millisecond )
@@ -278,8 +279,8 @@ func TestForwarder_PassThroughWaiting_HonorsProvidedTimeout(t *testing.T) {
278279 resp := f .PassThroughWaiting (50 * time .Millisecond , "/ready" , nil , nil )
279280 require .NotNil (t , resp )
280281 defer resp .Body .Close ()
281- assert .Equal (t , http .StatusGatewayTimeout , resp .StatusCode ,
282- "PassThroughWaiting must time out on the provided timeout" )
282+ assert .Equal (t , http .StatusServiceUnavailable , resp .StatusCode ,
283+ "PassThroughWaiting must time out on the provided timeout and return 503, never 504 " )
283284}
284285
285286// On the happy path the response body is wrapped by cancelOnCloseReader.
@@ -328,29 +329,33 @@ func TestWrapResponseBody_CapZero_DisablesCap(t *testing.T) {
328329 "cap=0 must read the full body — LimitReader must NOT be applied" )
329330}
330331
331- // PassThroughWaiting retries TCP dials until the context expires when the port
332- // is unbound, so the response is always 504 (deadline exceeded) — never 503.
333- // This is distinct from PassThrough which returns 503 on the very first
334- // connect-refused error. The behavioral difference is intentional: /ready and
335- // /validate must wait for the user app to start, not fail-fast on a transient
336- // dial error .
337- func TestForwarder_PassThroughWaiting_UnboundPort_RetriesUntilTimeout504 (t * testing.T ) {
332+ // PassThroughWaiting makes a single reachability dial attempt, bounded by
333+ // dialCheckTimeout — it must not retry/poll, and must not wait out the
334+ // timeout parameter, before answering. Per the hook contract the platform
335+ // (not the hook) owns the retry loop for /ready and /validate, so an
336+ // unreachable app must return 503 fast, the same fail-fast contract
337+ // PassThrough already applies to connect-refused errors .
338+ func TestForwarder_PassThroughWaiting_UnboundPort_FailsFastWith503 (t * testing.T ) {
338339 f := & Forwarder {
339- target : "http://127.0.0.1:1" , // unbound — every dial attempt is refused
340+ target : "http://127.0.0.1:1" , // unbound — dial attempt is refused
340341 client : & http.Client {},
341342 }
343+ start := time .Now ()
342344 resp := f .PassThroughWaiting (150 * time .Millisecond , "/ready" , nil , nil )
345+ elapsed := time .Since (start )
343346 require .NotNil (t , resp )
344347 defer resp .Body .Close ()
345- assert .Equal (t , http .StatusGatewayTimeout , resp .StatusCode ,
346- "unbound port must retry until timeout (504), not immediately 503 like PassThrough" )
348+ assert .Equal (t , http .StatusServiceUnavailable , resp .StatusCode ,
349+ "unreachable app must return 503, not 504" )
350+ assert .Less (t , elapsed , 100 * time .Millisecond ,
351+ "an unreachable app must fail the single dial attempt fast, not retry until the timeout parameter (150ms) or dialCheckTimeout (200ms) elapse" )
347352}
348353
349- // passThroughWaiting buffers the request body before the TCP wait so it is
350- // still available after waitForUserApp returns. Without buffering, the body
351- // reader would be exhausted during the wait loop and f.do would send an empty
352- // body to the user app. This pins the buffer-then-forward contract.
353- func TestForwarder_PassThroughWaiting_ForwardsBodyAfterTCPWait (t * testing.T ) {
354+ // passThroughWaiting buffers the request body before the reachability check
355+ // so it is still available once that check returns. Without buffering, the
356+ // body reader would be exhausted during the dial attempt and f.do would send
357+ // an empty body to the user app. This pins the buffer-then-forward contract.
358+ func TestForwarder_PassThroughWaiting_ForwardsBodyAfterReachabilityCheck (t * testing.T ) {
354359 received := make (chan []byte , 1 )
355360 srv := httptest .NewServer (http .HandlerFunc (func (_ http.ResponseWriter , r * http.Request ) {
356361 b , _ := io .ReadAll (r .Body )
@@ -380,7 +385,8 @@ func (errReader) Read([]byte) (int, error) { return 0, io.ErrUnexpectedEOF }
380385
381386// A failed read of the inbound body must NOT forward a partial body to the
382387// user app. PassThroughWaiting returns 500 (server-side read failure) and
383- // short-circuits before the TCP wait so no partial body reaches the user app.
388+ // short-circuits before the reachability check so no partial body reaches the
389+ // user app.
384390func TestForwarder_PassThroughWaiting_BodyReadError_Returns500 (t * testing.T ) {
385391 var reached atomic.Int32
386392 srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , _ * http.Request ) {
0 commit comments