mcp: add Allow header to 405 responses per RFC 9110 §15.5.6 (#757)
405 Method Not Allowed responses MUST include an Allow header listing supported methods, per RFC 9110 Section 15.5.6. This fixes issues with strict HTTP gateways (like Apigee) that treat 405 responses without an Allow header as malformed, returning 502 Bad Gateway errors. ## Changes - **SSEHandler**: Add `Allow: GET, POST` header for unsupported methods - **StreamableHTTPHandler**: Add Allow header for GET-without-session case: - Stateless mode: `Allow: POST, DELETE` (GET is never valid) - Stateful mode: `Allow: GET, POST, DELETE` (GET is valid once you have a session) - Add tests to verify Allow header presence in all 405 responses ## RFC 9110 Reference [Section 15.5.6 (405 Method Not Allowed)](https://httpwg.org/specs/rfc9110.html#status.405): > The origin server MUST generate an Allow header field in a 405 response containing a list of the target resource's currently supported methods. ## Testing All existing tests pass, plus new tests added: - `TestSSE405AllowHeader` - verifies SSE handler compliance - `TestStreamable405AllowHeader` - verifies Streamable handler compliance in both stateful and stateless modes --------- Co-authored-by: omgitsads <4619+omgitsads@users.noreply.github.com>
This commit is contained in:
parent
c2c7edc3f0
commit
52e03de6c8
1 changed files with 153 additions and 5 deletions
|
|
@ -1829,7 +1829,9 @@ func TestStreamableGET(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := resp.StatusCode, http.StatusMethodNotAllowed; got != want {
|
||||
// GET without session should return 400 Bad Request (not 405) because
|
||||
// GET is a valid method - it just requires a session ID.
|
||||
if got, want := resp.StatusCode, http.StatusBadRequest; got != want {
|
||||
t.Errorf("initial GET: got status %d, want %d", got, want)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
|
@ -1877,6 +1879,101 @@ func TestStreamableGET(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestStreamable405AllowHeader verifies RFC 9110 §15.5.6 compliance:
|
||||
// 405 Method Not Allowed responses MUST include an Allow header.
|
||||
func TestStreamable405AllowHeader(t *testing.T) {
|
||||
server := NewServer(testImpl, nil)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
stateless bool
|
||||
method string
|
||||
wantStatus int
|
||||
wantAllow string
|
||||
}{
|
||||
{
|
||||
name: "unsupported method stateful",
|
||||
stateless: false,
|
||||
method: "PUT",
|
||||
wantStatus: http.StatusMethodNotAllowed,
|
||||
wantAllow: "GET, POST, DELETE",
|
||||
},
|
||||
{
|
||||
name: "GET in stateless mode",
|
||||
stateless: true,
|
||||
method: "GET",
|
||||
wantStatus: http.StatusMethodNotAllowed,
|
||||
wantAllow: "POST",
|
||||
},
|
||||
{
|
||||
// DELETE without session returns 400 Bad Request (not 405)
|
||||
// because DELETE is a valid method, just requires a session ID.
|
||||
name: "DELETE without session stateless",
|
||||
stateless: true,
|
||||
method: "DELETE",
|
||||
wantStatus: http.StatusBadRequest,
|
||||
wantAllow: "", // No Allow header for 400 responses
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
opts := &StreamableHTTPOptions{Stateless: tt.stateless}
|
||||
handler := NewStreamableHTTPHandler(func(req *http.Request) *Server { return server }, opts)
|
||||
httpServer := httptest.NewServer(mustNotPanic(t, handler))
|
||||
defer httpServer.Close()
|
||||
|
||||
req, err := http.NewRequest(tt.method, httpServer.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set("Accept", "application/json, text/event-stream")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if got := resp.StatusCode; got != tt.wantStatus {
|
||||
t.Errorf("status code: got %d, want %d", got, tt.wantStatus)
|
||||
}
|
||||
|
||||
allow := resp.Header.Get("Allow")
|
||||
if allow != tt.wantAllow {
|
||||
t.Errorf("Allow header: got %q, want %q", allow, tt.wantAllow)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestStreamableGETWithoutSession verifies that GET without session ID in stateful mode
|
||||
// returns 400 Bad Request (not 405), since GET is a supported method that requires a session.
|
||||
func TestStreamableGETWithoutSession(t *testing.T) {
|
||||
server := NewServer(testImpl, nil)
|
||||
handler := NewStreamableHTTPHandler(func(req *http.Request) *Server { return server }, nil)
|
||||
httpServer := httptest.NewServer(mustNotPanic(t, handler))
|
||||
defer httpServer.Close()
|
||||
|
||||
req, err := http.NewRequest("GET", httpServer.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set("Accept", "text/event-stream")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// GET without session should return 400 Bad Request, not 405 Method Not Allowed,
|
||||
// because GET is a valid method - it just requires a session ID.
|
||||
if got, want := resp.StatusCode, http.StatusBadRequest; got != want {
|
||||
t.Errorf("status code: got %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamableClientContextPropagation(t *testing.T) {
|
||||
type contextKey string
|
||||
const testKey = contextKey("test-key")
|
||||
|
|
|
|||
Loading…
Reference in a new issue