@@ -773,28 +773,27 @@ func TestHandleStreamProviderErrorSurfacesRealCause(t *testing.T) {
773773 }}
774774 b := newFakeBridge (t , gw )
775775
776+ // The failure frames carry no content, so nothing is committed to the
777+ // client and the rejection must surface as a real HTTP status carrying the
778+ // embedded 429 — not a 200 SSE stream whose success every SDK would read.
779+ handler := MakeChatHandler (func (string ) * OpenAiBridge { return b }, nil )
780+ req := httptest .NewRequest ("POST" , "/v1/chat/completions" , strings .NewReader (
781+ `{"model":"Fake-Model","messages":[{"role":"user","content":"hi"}],"stream":true}` ))
782+ req .Header .Set ("Authorization" , "Bearer sk-test" )
776783 w := httptest .NewRecorder ()
777- err := b .HandleChat (context .Background (), w , map [string ]interface {}{
778- "model" : "Fake-Model" ,
779- "messages" : []interface {}{map [string ]interface {}{"role" : "user" , "content" : "hi" }},
780- "stream" : true ,
781- }, nil )
782- if err == nil {
783- t .Fatal ("provider outage stream must fail" )
784- }
785- var repErr * StreamReportedError
786- if ! errors .As (err , & repErr ) {
787- t .Fatalf ("expected StreamReportedError, got %T: %v" , err , err )
784+ handler (w , req )
785+
786+ if w .Code != http .StatusTooManyRequests {
787+ t .Fatalf ("status = %d, want 429 (embedded statusCodeValue)\n %s" , w .Code , w .Body .String ())
788788 }
789- body := w .Body .String ()
790- if ! strings .Contains (body , "HTTP 429" ) || ! strings .Contains (body , "provider_error: All backends failed" ) {
791- t .Errorf ("error chunk must surface the real gateway cause, got:\n %s" , body )
789+ if ! strings .Contains (w .Body .String (), "provider_error: All backends failed" ) {
790+ t .Errorf ("error body must surface the real gateway cause, got:\n %s" , w .Body .String ())
792791 }
793- if strings .Contains (body , "connection truncated" ) {
794- t .Errorf ("misleading truncated message must be gone, got:\n %s" , body )
792+ if strings .Contains (w . Body . String () , "connection truncated" ) {
793+ t .Errorf ("misleading truncated message must be gone, got:\n %s" , w . Body . String () )
795794 }
796- if strings .HasSuffix ( strings . TrimSpace ( body ) , "data: [DONE] " ) {
797- t .Error ( "failed stream must not terminate with [DONE] " )
795+ if strings .Contains ( w . Header (). Get ( "Content-Type" ) , "text/event-stream " ) {
796+ t .Errorf ( "no content was delivered; the response must be a JSON error, not SSE " )
798797 }
799798}
800799
@@ -1141,18 +1140,22 @@ func TestMakeChatHandlerMapsBusyTo429(t *testing.T) {
11411140 t .Errorf ("Retry-After header missing/invalid: %q" , ra )
11421141 }
11431142
1144- // Streaming path: the error chunk carries the busy message and no second
1145- // HTTP-level response is attempted.
1143+ // Streaming path: with no content delivered yet the response is still
1144+ // uncommitted, so the busy rejection must surface as a real 429 (with
1145+ // Retry-After) instead of an already-committed 200 SSE stream.
11461146 w2 := httptest .NewRecorder ()
11471147 body := `{"model":"Fake-Model","messages":[{"role":"user","content":"hi"}],"stream":true}`
11481148 req2 := httptest .NewRequest ("POST" , "/v1/chat/completions" , strings .NewReader (body ))
11491149 req2 .Header .Set ("Authorization" , "Bearer sk-test" )
11501150 handler (w2 , req2 )
1151- if w2 .Code != 200 {
1152- t .Errorf ("streaming status = %d, want 200 (SSE headers already sent)" , w2 .Code )
1151+ if w2 .Code != http .StatusTooManyRequests {
1152+ t .Errorf ("streaming status = %d, want 429 (no content committed yet)\n %s" , w2 .Code , w2 .Body .String ())
1153+ }
1154+ if ra := w2 .Header ().Get ("Retry-After" ); ra == "" || ra == "0" {
1155+ t .Errorf ("streaming Retry-After header missing/invalid: %q" , ra )
11531156 }
1154- if ! strings .Contains (w2 .Body .String (), "10605 " ) {
1155- t .Errorf ("stream body should carry the busy code :\n %s" , w2 .Body .String ())
1157+ if ! strings .Contains (w2 .Body .String (), "upstream busy " ) {
1158+ t .Errorf ("stream body should carry the busy message :\n %s" , w2 .Body .String ())
11561159 }
11571160}
11581161
@@ -1830,8 +1833,9 @@ func TestFmtResetTime(t *testing.T) {
18301833// rejectingGateway serves the auth endpoints but rejects every chat request
18311834// with a fixed status and body, for pass-through error mapping tests.
18321835type rejectingGateway struct {
1833- status int
1834- body string
1836+ status int
1837+ body string
1838+ retryAfter string // optional Retry-After header served with the rejection
18351839}
18361840
18371841func newRejectingBridge (t * testing.T , gw * rejectingGateway ) * OpenAiBridge {
@@ -1847,6 +1851,9 @@ func newRejectingBridge(t *testing.T, gw *rejectingGateway) *OpenAiBridge {
18471851 })
18481852 mux .HandleFunc ("/algo/api/v2/service/pro/sse/agent_chat_generation" , func (w http.ResponseWriter , r * http.Request ) {
18491853 w .Header ().Set ("Content-Type" , "application/json" )
1854+ if gw .retryAfter != "" {
1855+ w .Header ().Set ("Retry-After" , gw .retryAfter )
1856+ }
18501857 w .WriteHeader (gw .status )
18511858 io .WriteString (w , gw .body )
18521859 })
@@ -1860,31 +1867,30 @@ func newRejectingBridge(t *testing.T, gw *rejectingGateway) *OpenAiBridge {
18601867// real status and message instead of being masked as a bridge 500.
18611868func TestMakeChatHandlerMapsUpstreamErrors (t * testing.T ) {
18621869 cases := []struct {
1863- name string
1864- status int
1865- body string
1866- wantCode int
1867- wantInBody string
1870+ name string
1871+ status int
1872+ body string
1873+ retryAfter string // Retry-After header served by the gateway, "" for none
1874+ wantCode int
1875+ wantInBody string
1876+ wantRetryAfter string
18681877 }{
18691878 {"400 parameter rejection passes through" ,
1870- 400 , `{"code":"invalid_parameter_error","message":"Range of max_tokens validation failed"}` ,
1871- 400 , "Range of max_tokens" },
1879+ 400 , `{"code":"invalid_parameter_error","message":"Range of max_tokens validation failed"}` , "" , 400 , "Range of max_tokens" , "" },
18721880 {"404 unknown model" ,
1873- 404 , `{"code":"not_found","message":"model not found"}` ,
1874- 400 , "model not found" },
1881+ 404 , `{"code":"not_found","message":"model not found"}` , "" , 400 , "model not found" , "" },
18751882 {"429 rate limit" ,
1876- 429 , `{"code":"too_many_requests","message":"slow down"}` ,
1877- 429 , "slow down" },
1883+ 429 , `{"code":"too_many_requests","message":"slow down"}` , "" , 429 , "slow down" , "" },
1884+ {"429 rate limit with Retry-After passes it through" ,
1885+ 429 , `{"code":"too_many_requests","message":"slow down"}` , "7" , 429 , "slow down" , "7" },
18781886 {"500 gateway fault maps to 502" ,
1879- 500 , `{"code":"internal_error","message":"boom"}` ,
1880- 502 , "boom" },
1887+ 500 , `{"code":"internal_error","message":"boom"}` , "" , 502 , "boom" , "" },
18811888 {"503 gateway unavailable maps to 502" ,
1882- 503 , "service unavailable" ,
1883- 502 , "service unavailable" },
1889+ 503 , "service unavailable" , "" , 502 , "service unavailable" , "" },
18841890 }
18851891 for _ , tc := range cases {
18861892 t .Run (tc .name , func (t * testing.T ) {
1887- gw := & rejectingGateway {status : tc .status , body : tc .body }
1893+ gw := & rejectingGateway {status : tc .status , body : tc .body , retryAfter : tc . retryAfter }
18881894 b := newRejectingBridge (t , gw )
18891895 handler := MakeChatHandler (func (string ) * OpenAiBridge { return b }, nil )
18901896
@@ -1900,6 +1906,9 @@ func TestMakeChatHandlerMapsUpstreamErrors(t *testing.T) {
19001906 if ! strings .Contains (w .Body .String (), tc .wantInBody ) {
19011907 t .Errorf ("body must carry the upstream message %q:\n %s" , tc .wantInBody , w .Body .String ())
19021908 }
1909+ if got := w .Header ().Get ("Retry-After" ); got != tc .wantRetryAfter {
1910+ t .Errorf ("Retry-After = %q, want %q" , got , tc .wantRetryAfter )
1911+ }
19031912 })
19041913 }
19051914}
0 commit comments