diff --git a/.changeset/exact-event-stream-type.md b/.changeset/exact-event-stream-type.md new file mode 100644 index 0000000..b2c4fee --- /dev/null +++ b/.changeset/exact-event-stream-type.md @@ -0,0 +1,5 @@ +--- +'eventsource': patch +--- + +Accept case variations of `text/event-stream` and reject different MIME subtypes such as `text/event-streaming`. diff --git a/src/EventSource.ts b/src/EventSource.ts index e4083b4..75ed50f 100644 --- a/src/EventSource.ts +++ b/src/EventSource.ts @@ -484,7 +484,7 @@ class EventSourceImpl extends EventTarget implements EventSource { // [spec] …or if res's `Content-Type` is not `text/event-stream`, then fail the connection. const contentType = headers.get('content-type') || '' - if (!contentType.startsWith('text/event-stream')) { + if (!/^[\t\n\r ]*text\/event-stream[\t\n\r ]*(?:;|$)/i.test(contentType)) { this.#failConnection('Invalid content type, expected "text/event-stream"', status) return } diff --git a/test/client.test.ts b/test/client.test.ts index e26a891..546cada 100644 --- a/test/client.test.ts +++ b/test/client.test.ts @@ -856,6 +856,38 @@ browserTest( }, ) +test.each([ + {contentType: 'Text/Event-Stream', valid: true}, + {contentType: 'TEXT/EVENT-STREAM; charset=utf-8', valid: true}, + {contentType: 'text/event-stream;charset=utf-8', valid: true}, + {contentType: ' \ttext/event-stream\t ; charset=iso-8859-1', valid: true}, + {contentType: 'text/event-stream; note="a,b"', valid: true}, + {contentType: 'text/event-stream;', valid: true}, + {contentType: 'text/event-streaming', valid: false}, + {contentType: 'text/event-stream+json', valid: false}, + {contentType: 'text/event-stream/extra', valid: false}, + {contentType: 'text/event-stream garbage', valid: false}, + {contentType: 'text /event-stream', valid: false}, + {contentType: 'text/ event-stream', valid: false}, + {contentType: 'text/event-stream\u00a0; charset=utf-8', valid: false}, +])('validates the event stream MIME type: $contentType', async ({contentType, valid}) => { + const onResult = getCallCounter({name: 'MIME result'}) + const es = new OurEventSource( + `${serverUrl}/?${new URLSearchParams({'content-type': contentType})}`, + esInit, + ) + es.addEventListener('welcome', onResult.listener) + es.addEventListener('error', onResult.listener) + + try { + await onResult.waitForCallCount(1) + expect(onResult.lastArg.type).toBe(valid ? 'welcome' : 'error') + expect(es.readyState).toBe(valid ? OurEventSource.OPEN : OurEventSource.CLOSED) + } finally { + es.close() + } +}) + test.each([200, 403])( 'aborts a rejected HTTP %i response before reporting failure', async (status) => { diff --git a/test/helpers/server.ts b/test/helpers/server.ts index a8e0646..9798e2c 100644 --- a/test/helpers/server.ts +++ b/test/helpers/server.ts @@ -100,9 +100,12 @@ export function handleRequest( } } -function writeDefault(_req: IncomingMessage, res: ServerResponse) { +function writeDefault(req: IncomingMessage, res: ServerResponse) { + const contentType = + new URL(req.url || '/', 'http://localhost').searchParams.get('content-type') ?? + 'text/event-stream' res.writeHead(200, { - 'Content-Type': 'text/event-stream', + 'Content-Type': contentType, 'Cache-Control': 'no-cache', Connection: 'keep-alive', })