From e8f2069f3b033572cf09b93a52f1890a10e84629 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 10 Aug 2026 14:20:24 -0400 Subject: [PATCH 1/3] feat(http-specs): add SSE protocol scenarios Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ee116a23-26c2-4be5-b305-6c2733ad0790 --- packages/http-specs/spec-summary.md | 81 ++++++++++++++++ .../http-specs/specs/streaming/sse/main.tsp | 94 +++++++++++++++++++ .../http-specs/specs/streaming/sse/mockapi.ts | 89 +++++++++++++++++- 3 files changed, 263 insertions(+), 1 deletion(-) diff --git a/packages/http-specs/spec-summary.md b/packages/http-specs/spec-summary.md index 77e08a1490f..995ab1a9d86 100644 --- a/packages/http-specs/spec-summary.md +++ b/packages/http-specs/spec-summary.md @@ -5387,6 +5387,87 @@ data: [DONE] ``` +### Streaming_Sse_Protocol_id + +- Endpoint: `get /streaming/sse/protocol/id` + +An SSE event with an `id` field. The event ID is envelope metadata and is not +part of the typed event data. + +Expected response body (content type `text/event-stream`): + +``` +id: event-1 +data: {"message": "hello"} + +``` + +### Streaming_Sse_Protocol_invalidId + +- Endpoint: `get /streaming/sse/protocol/invalid-id` + +An SSE event with an `id` field containing U+0000 NULL. The field is ignored +according to the SSE parsing rules. + +Expected response body (content type `text/event-stream`): + +``` +id: invalid\u0000id +data: {"message": "hello"} + +``` + +### Streaming_Sse_Protocol_invalidRetry + +- Endpoint: `get /streaming/sse/protocol/invalid-retry` + +An SSE event with an invalid `retry` field. Since the value contains +non-ASCII-digit characters, the field is ignored. + +Expected response body (content type `text/event-stream`): + +``` +retry: not-a-number +data: {"message": "hello"} + +``` + +### Streaming_Sse_Protocol_reconnect + +- Endpoint: `get /streaming/sse/protocol/reconnect` + +An SSE stream that resumes after a reconnect. The client sends the most +recently received event ID in the `Last-Event-ID` request header. + +Expected request header: + +``` +Last-Event-ID: event-1 +``` + +Expected response body (content type `text/event-stream`): + +``` +id: event-2 +data: {"message": "world"} + +``` + +### Streaming_Sse_Protocol_retry + +- Endpoint: `get /streaming/sse/protocol/retry` + +An SSE event with a valid `retry` field containing only ASCII digits. The field +sets the client's reconnection delay and is not part of the typed event data. + +Expected response body (content type `text/event-stream`): + +``` +retry: 1000 +data: {"message": "hello"} + +``` + ### Streaming_Sse_Unnamed_receive - Endpoint: `get /streaming/sse/unnamed/receive` diff --git a/packages/http-specs/specs/streaming/sse/main.tsp b/packages/http-specs/specs/streaming/sse/main.tsp index 16e81a33deb..95264225c2f 100644 --- a/packages/http-specs/specs/streaming/sse/main.tsp +++ b/packages/http-specs/specs/streaming/sse/main.tsp @@ -151,3 +151,97 @@ namespace Retrieve { @route("stream") op stream(@body request: RetrievalRequest): SSEStream; } + +@route("protocol") +namespace Protocol { + model Info { + message: string; + } + + @events + union ProtocolEvents { + @Events.contentType("application/json") + Info, + } + + @scenario + @scenarioDoc(""" + An SSE event with an `id` field. The event ID is envelope metadata and is + not part of the typed event data. + + Expected response body (content type `text/event-stream`): + ``` + id: event-1 + data: {"message": "hello"} + + ``` + """) + @route("id") + op id(): SSEStream; + + @scenario + @scenarioDoc(""" + An SSE event with an `id` field containing U+0000 NULL. The field is + ignored according to the SSE parsing rules. + + Expected response body (content type `text/event-stream`): + ``` + id: invalid\u0000id + data: {"message": "hello"} + + ``` + """) + @route("invalid-id") + op invalidId(): SSEStream; + + @scenario + @scenarioDoc(""" + An SSE event with a valid `retry` field containing only ASCII digits. The + field sets the client's reconnection delay and is not part of the typed + event data. + + Expected response body (content type `text/event-stream`): + ``` + retry: 1000 + data: {"message": "hello"} + + ``` + """) + @route("retry") + op retry(): SSEStream; + + @scenario + @scenarioDoc(""" + An SSE event with an invalid `retry` field. Since the value contains + non-ASCII-digit characters, the field is ignored. + + Expected response body (content type `text/event-stream`): + ``` + retry: not-a-number + data: {"message": "hello"} + + ``` + """) + @route("invalid-retry") + op invalidRetry(): SSEStream; + + @scenario + @scenarioDoc(""" + An SSE stream that resumes after a reconnect. The client sends the most + recently received event ID in the `Last-Event-ID` request header. + + Expected request header: + ``` + Last-Event-ID: event-1 + ``` + + Expected response body (content type `text/event-stream`): + ``` + id: event-2 + data: {"message": "world"} + + ``` + """) + @route("reconnect") + op reconnect(): SSEStream; +} diff --git a/packages/http-specs/specs/streaming/sse/mockapi.ts b/packages/http-specs/specs/streaming/sse/mockapi.ts index d74f321d927..1484539d2f7 100644 --- a/packages/http-specs/specs/streaming/sse/mockapi.ts +++ b/packages/http-specs/specs/streaming/sse/mockapi.ts @@ -3,7 +3,11 @@ import { passOnSuccess } from "@typespec/spec-api"; export const Scenarios: Record = {}; -const unnamedStream = ['data: {"desc": "one"}', 'data: {"desc": "two"}', 'data: {"desc": "three"}'] +const unnamedStream = [ + 'data: {"desc": "one"}', + 'data: {"desc": "two"}', + 'data: {"desc": "three"}', +] .map((event) => `${event}\n\n`) .join(""); @@ -70,3 +74,86 @@ Scenarios.Streaming_Sse_Retrieve_stream = passOnSuccess({ }, kind: "MockApiDefinition", }); + +const protocolEvent = (fields: string[]) => + Buffer.from(`${fields.join("\n")}\n\n`); + +Scenarios.Streaming_Sse_Protocol_id = passOnSuccess({ + uri: "/streaming/sse/protocol/id", + method: "get", + request: {}, + response: { + status: 200, + body: { + rawContent: protocolEvent(["id: event-1", 'data: {"message": "hello"}']), + contentType: "text/event-stream", + }, + }, + kind: "MockApiDefinition", +}); + +Scenarios.Streaming_Sse_Protocol_invalidId = passOnSuccess({ + uri: "/streaming/sse/protocol/invalid-id", + method: "get", + request: {}, + response: { + status: 200, + body: { + rawContent: protocolEvent([ + "id: invalid\u0000id", + 'data: {"message": "hello"}', + ]), + contentType: "text/event-stream", + }, + }, + kind: "MockApiDefinition", +}); + +Scenarios.Streaming_Sse_Protocol_retry = passOnSuccess({ + uri: "/streaming/sse/protocol/retry", + method: "get", + request: {}, + response: { + status: 200, + body: { + rawContent: protocolEvent(["retry: 1000", 'data: {"message": "hello"}']), + contentType: "text/event-stream", + }, + }, + kind: "MockApiDefinition", +}); + +Scenarios.Streaming_Sse_Protocol_invalidRetry = passOnSuccess({ + uri: "/streaming/sse/protocol/invalid-retry", + method: "get", + request: {}, + response: { + status: 200, + body: { + rawContent: protocolEvent([ + "retry: not-a-number", + 'data: {"message": "hello"}', + ]), + contentType: "text/event-stream", + }, + }, + kind: "MockApiDefinition", +}); + +Scenarios.Streaming_Sse_Protocol_reconnect = passOnSuccess({ + uri: "/streaming/sse/protocol/reconnect", + method: "get", + request: { + headers: { + "last-event-id": "event-1", + }, + }, + response: { + status: 200, + body: { + rawContent: protocolEvent(["id: event-2", 'data: {"message": "world"}']), + contentType: "text/event-stream", + }, + }, + kind: "MockApiDefinition", +}); From 2645da88cefc4cd6379b06b6a57738ee6a184b76 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 10 Aug 2026 14:24:05 -0400 Subject: [PATCH 2/3] chore: add SSE specs changeset Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ee116a23-26c2-4be5-b305-6c2733ad0790 --- ...ai-msft-add-sse-spector-scenarios-2026-7-10-14-23-58.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/iscai-msft-add-sse-spector-scenarios-2026-7-10-14-23-58.md diff --git a/.chronus/changes/iscai-msft-add-sse-spector-scenarios-2026-7-10-14-23-58.md b/.chronus/changes/iscai-msft-add-sse-spector-scenarios-2026-7-10-14-23-58.md new file mode 100644 index 00000000000..0cfa45d7805 --- /dev/null +++ b/.chronus/changes/iscai-msft-add-sse-spector-scenarios-2026-7-10-14-23-58.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@typespec/http-specs" +--- + +Add SSE protocol coverage for event IDs, retry fields, and reconnection \ No newline at end of file From d35f7beb27a7b56c9c59f1a0172d9f70155b3672 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 10 Aug 2026 14:31:52 -0400 Subject: [PATCH 3/3] fix(http-specs): format SSE scenarios Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ee116a23-26c2-4be5-b305-6c2733ad0790 --- packages/http-specs/spec-summary.md | 77 ++++++++++--------- .../http-specs/specs/streaming/sse/main.tsp | 24 +++--- .../http-specs/specs/streaming/sse/mockapi.ts | 19 +---- 3 files changed, 55 insertions(+), 65 deletions(-) diff --git a/packages/http-specs/spec-summary.md b/packages/http-specs/spec-summary.md index 995ab1a9d86..2ae36494582 100644 --- a/packages/http-specs/spec-summary.md +++ b/packages/http-specs/spec-summary.md @@ -5356,43 +5356,12 @@ data: [DONE] ``` -### Streaming_Sse_Retrieve_stream - -- Endpoint: `post /streaming/sse/retrieve/stream` - -A POST request with a JSON body whose response is an SSE stream, modeled -after a knowledge-retrieval service. The server streams `partialResult` -events as results become available, a final `finalResult` event, and a -terminal `[DONE]` event. - -Expected request body (content type `application/json`): - -``` -{"query": "what is typespec?"} -``` - -Expected response body (content type `text/event-stream`): - -``` -event: partialResult -data: {"text": "partial one"} - -event: partialResult -data: {"text": "partial two"} - -event: finalResult -data: {"references": ["doc1", "doc2"]} - -data: [DONE] - -``` - ### Streaming_Sse_Protocol_id - Endpoint: `get /streaming/sse/protocol/id` -An SSE event with an `id` field. The event ID is envelope metadata and is not -part of the typed event data. +An SSE event with an `id` field. The event ID is envelope metadata and is +not part of the typed event data. Expected response body (content type `text/event-stream`): @@ -5406,13 +5375,13 @@ data: {"message": "hello"} - Endpoint: `get /streaming/sse/protocol/invalid-id` -An SSE event with an `id` field containing U+0000 NULL. The field is ignored -according to the SSE parsing rules. +An SSE event with an `id` field containing U+0000 NULL. The field is +ignored according to the SSE parsing rules. Expected response body (content type `text/event-stream`): ``` -id: invalid\u0000id +id: invalidid data: {"message": "hello"} ``` @@ -5457,8 +5426,9 @@ data: {"message": "world"} - Endpoint: `get /streaming/sse/protocol/retry` -An SSE event with a valid `retry` field containing only ASCII digits. The field -sets the client's reconnection delay and is not part of the typed event data. +An SSE event with a valid `retry` field containing only ASCII digits. The +field sets the client's reconnection delay and is not part of the typed +event data. Expected response body (content type `text/event-stream`): @@ -5468,6 +5438,37 @@ data: {"message": "hello"} ``` +### Streaming_Sse_Retrieve_stream + +- Endpoint: `post /streaming/sse/retrieve/stream` + +A POST request with a JSON body whose response is an SSE stream, modeled +after a knowledge-retrieval service. The server streams `partialResult` +events as results become available, a final `finalResult` event, and a +terminal `[DONE]` event. + +Expected request body (content type `application/json`): + +``` +{"query": "what is typespec?"} +``` + +Expected response body (content type `text/event-stream`): + +``` +event: partialResult +data: {"text": "partial one"} + +event: partialResult +data: {"text": "partial two"} + +event: finalResult +data: {"references": ["doc1", "doc2"]} + +data: [DONE] + +``` + ### Streaming_Sse_Unnamed_receive - Endpoint: `get /streaming/sse/unnamed/receive` diff --git a/packages/http-specs/specs/streaming/sse/main.tsp b/packages/http-specs/specs/streaming/sse/main.tsp index 95264225c2f..66f314f9e94 100644 --- a/packages/http-specs/specs/streaming/sse/main.tsp +++ b/packages/http-specs/specs/streaming/sse/main.tsp @@ -168,12 +168,12 @@ namespace Protocol { @scenarioDoc(""" An SSE event with an `id` field. The event ID is envelope metadata and is not part of the typed event data. - + Expected response body (content type `text/event-stream`): ``` id: event-1 data: {"message": "hello"} - + ``` """) @route("id") @@ -183,12 +183,12 @@ namespace Protocol { @scenarioDoc(""" An SSE event with an `id` field containing U+0000 NULL. The field is ignored according to the SSE parsing rules. - + Expected response body (content type `text/event-stream`): ``` - id: invalid\u0000id + id: invalidid data: {"message": "hello"} - + ``` """) @route("invalid-id") @@ -199,12 +199,12 @@ namespace Protocol { An SSE event with a valid `retry` field containing only ASCII digits. The field sets the client's reconnection delay and is not part of the typed event data. - + Expected response body (content type `text/event-stream`): ``` retry: 1000 data: {"message": "hello"} - + ``` """) @route("retry") @@ -214,12 +214,12 @@ namespace Protocol { @scenarioDoc(""" An SSE event with an invalid `retry` field. Since the value contains non-ASCII-digit characters, the field is ignored. - + Expected response body (content type `text/event-stream`): ``` retry: not-a-number data: {"message": "hello"} - + ``` """) @route("invalid-retry") @@ -229,17 +229,17 @@ namespace Protocol { @scenarioDoc(""" An SSE stream that resumes after a reconnect. The client sends the most recently received event ID in the `Last-Event-ID` request header. - + Expected request header: ``` Last-Event-ID: event-1 ``` - + Expected response body (content type `text/event-stream`): ``` id: event-2 data: {"message": "world"} - + ``` """) @route("reconnect") diff --git a/packages/http-specs/specs/streaming/sse/mockapi.ts b/packages/http-specs/specs/streaming/sse/mockapi.ts index 1484539d2f7..3bab7b4598b 100644 --- a/packages/http-specs/specs/streaming/sse/mockapi.ts +++ b/packages/http-specs/specs/streaming/sse/mockapi.ts @@ -3,11 +3,7 @@ import { passOnSuccess } from "@typespec/spec-api"; export const Scenarios: Record = {}; -const unnamedStream = [ - 'data: {"desc": "one"}', - 'data: {"desc": "two"}', - 'data: {"desc": "three"}', -] +const unnamedStream = ['data: {"desc": "one"}', 'data: {"desc": "two"}', 'data: {"desc": "three"}'] .map((event) => `${event}\n\n`) .join(""); @@ -75,8 +71,7 @@ Scenarios.Streaming_Sse_Retrieve_stream = passOnSuccess({ kind: "MockApiDefinition", }); -const protocolEvent = (fields: string[]) => - Buffer.from(`${fields.join("\n")}\n\n`); +const protocolEvent = (fields: string[]) => Buffer.from(`${fields.join("\n")}\n\n`); Scenarios.Streaming_Sse_Protocol_id = passOnSuccess({ uri: "/streaming/sse/protocol/id", @@ -99,10 +94,7 @@ Scenarios.Streaming_Sse_Protocol_invalidId = passOnSuccess({ response: { status: 200, body: { - rawContent: protocolEvent([ - "id: invalid\u0000id", - 'data: {"message": "hello"}', - ]), + rawContent: protocolEvent(["id: invalid\u0000id", 'data: {"message": "hello"}']), contentType: "text/event-stream", }, }, @@ -130,10 +122,7 @@ Scenarios.Streaming_Sse_Protocol_invalidRetry = passOnSuccess({ response: { status: 200, body: { - rawContent: protocolEvent([ - "retry: not-a-number", - 'data: {"message": "hello"}', - ]), + rawContent: protocolEvent(["retry: not-a-number", 'data: {"message": "hello"}']), contentType: "text/event-stream", }, },