Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@typespec/http-specs"
---

Add SSE protocol coverage for event IDs, retry fields, and reconnection
82 changes: 82 additions & 0 deletions packages/http-specs/spec-summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -5356,6 +5356,88 @@ 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<U+0000 NULL>id
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_Retrieve_stream

- Endpoint: `post /streaming/sse/retrieve/stream`
Expand Down
94 changes: 94 additions & 0 deletions packages/http-specs/specs/streaming/sse/main.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,97 @@ namespace Retrieve {
@route("stream")
op stream(@body request: RetrievalRequest): SSEStream<RetrievalEvents>;
}

@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<ProtocolEvents>;

@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<U+0000 NULL>id
data: {"message": "hello"}

```
""")
@route("invalid-id")
op invalidId(): SSEStream<ProtocolEvents>;

@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<ProtocolEvents>;

@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<ProtocolEvents>;

@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<ProtocolEvents>;
}
76 changes: 76 additions & 0 deletions packages/http-specs/specs/streaming/sse/mockapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,79 @@ 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",
});
Loading