diff --git a/.changeset/allow-header-405.md b/.changeset/allow-header-405.md new file mode 100644 index 00000000..bd0c54af --- /dev/null +++ b/.changeset/allow-header-405.md @@ -0,0 +1,5 @@ +--- +"@chatcops/server": patch +--- + +405 responses now include `Allow: POST, OPTIONS`. diff --git a/packages/server/src/adapters/express.ts b/packages/server/src/adapters/express.ts index 95814e3a..9622e277 100644 --- a/packages/server/src/adapters/express.ts +++ b/packages/server/src/adapters/express.ts @@ -42,6 +42,7 @@ export function chatcopsMiddleware(config: ChatCopsServerConfig) { } if (req.method !== 'POST') { + res.setHeader('Allow', 'POST, OPTIONS'); sendError(405, 'Method not allowed'); return; } diff --git a/packages/server/src/http.ts b/packages/server/src/http.ts index 84315b53..bfc4374d 100644 --- a/packages/server/src/http.ts +++ b/packages/server/src/http.ts @@ -257,7 +257,7 @@ export async function fetchChatResponse( if (request.method === 'OPTIONS') { return new Response(null, { status: 204, headers: origins.preflightHeaders(origin) }); } - if (request.method !== 'POST') return json(405, { error: 'Method not allowed' }); + if (request.method !== 'POST') return json(405, { error: 'Method not allowed' }, { Allow: 'POST, OPTIONS' }); if (!origins.allows(origin)) return json(403, { error: 'origin_not_allowed' }); if (isSessionPath(new URL(request.url).pathname)) { diff --git a/packages/server/tests/adapters/cloudflare.test.ts b/packages/server/tests/adapters/cloudflare.test.ts index 94749412..ba7abe43 100644 --- a/packages/server/tests/adapters/cloudflare.test.ts +++ b/packages/server/tests/adapters/cloudflare.test.ts @@ -25,6 +25,7 @@ describe('chatcopsCloudflareHandler', () => { const res = await handler(req); expect(res.status).toBe(405); expect(res.headers.get('Access-Control-Allow-Origin')).toBe('*'); + expect(res.headers.get('Allow')).toBe('POST, OPTIONS'); const body = await res.json(); expect(body.error).toBe('Method not allowed'); }); diff --git a/packages/server/tests/adapters/express.test.ts b/packages/server/tests/adapters/express.test.ts index 4c08160a..e5f46dc9 100644 --- a/packages/server/tests/adapters/express.test.ts +++ b/packages/server/tests/adapters/express.test.ts @@ -254,6 +254,7 @@ describe('express analytics sharing', () => { expect(fake.statusCode).toBe(405); expect(fake.headers['Access-Control-Allow-Origin']).toBe('*'); + expect(fake.headers.Allow).toBe('POST, OPTIONS'); expect(fake.body).toEqual({ error: 'Method not allowed' }); expect(fake.chunks).toEqual([]); expect(fake.ended).toBe(false); diff --git a/packages/server/tests/adapters/vercel.test.ts b/packages/server/tests/adapters/vercel.test.ts index 6d996e96..d2521206 100644 --- a/packages/server/tests/adapters/vercel.test.ts +++ b/packages/server/tests/adapters/vercel.test.ts @@ -25,6 +25,7 @@ describe('chatcopsVercelHandler', () => { const res = await handler(req); expect(res.status).toBe(405); expect(res.headers.get('Access-Control-Allow-Origin')).toBe('*'); + expect(res.headers.get('Allow')).toBe('POST, OPTIONS'); const body = await res.json(); expect(body.error).toBe('Method not allowed'); }); diff --git a/website/src/content/docs/server/protocol.mdx b/website/src/content/docs/server/protocol.mdx index 52e13f91..b85035df 100644 --- a/website/src/content/docs/server/protocol.mdx +++ b/website/src/content/docs/server/protocol.mdx @@ -345,7 +345,7 @@ Access-Control-Expose-Headers: Retry-After, X-ChatCops-Protocol | `401` | `conversation_mismatch` | The session token is valid but was issued for a different conversation. | Obtain a token for the current conversation from `/session`. | | `401` | `turnstile_failed` | `/session` endpoint only: Cloudflare Turnstile verification failed or was missing. | Returned when bot protection challenge fails. | | `403` | `origin_not_allowed` | The `Origin` header is not permitted by `allowedOrigins`. | Answered before the request body is parsed. Returns legacy format `{"error":"origin_not_allowed"}`. | -| `405` | `Method not allowed` | An HTTP method other than `POST` or `OPTIONS` was used on the chat endpoint. | Answered before the request body is parsed. Returns legacy format `{"error":"Method not allowed"}`. | +| `405` | `Method not allowed` | An HTTP method other than `POST` or `OPTIONS` was used on the chat endpoint. | Answered before the request body is parsed, with `Allow: POST, OPTIONS`. Returns legacy format `{"error":"Method not allowed"}`. | | `409` | `conversation_busy` | Another reply is currently being generated for this `conversationId`. | The server locks conversation turns to prevent state races. | | `413` | `payload_too_large` | The request body exceeds the maximum permitted size (default 256 KB). | Prevents excessive memory allocation. | | `429` | `rate_limited` | The client IP has exceeded the configured request rate limit. | Includes `retryAfterMs` in body and `Retry-After` in HTTP headers. |