
Research
/Security News
Mini Shai-Hulud Campaign Hits Red Hat Cloud Services npm Packages
A mini Shai-Hulud campaign compromised Red Hat Cloud Services npm packages to steal developer and CI/CD secrets during installation.
@bdky/ky-sse-hook
Advanced tools
English | 简体中文
A lightweight afterResponse hook for ky that handles Server-Sent Events (SSE) streaming. Built on top of eventsource-parser for spec-compliant SSE parsing.
Perfect for AI chat streaming, real-time data feeds, and any scenario where you need to consume SSE responses through ky.
hooks.afterResponseonData / onCompleted / onAborted / onEvent / onMessage / onReconnectIntervalonCompleted fires at most onceAbortController signals and routes to onAborted# npm
npm install @bdky/ky-sse-hook
# yarn
yarn add @bdky/ky-sse-hook
# pnpm
pnpm add @bdky/ky-sse-hook
Peer dependency:
ky >= 1.0.0is required. Install it separately if you haven't already.
The package ships both ESM and CJS formats with TypeScript declarations.
import ky from 'ky';
import {createHook} from '@bdky/ky-sse-hook';
const hook = createHook({
onData(message) {
console.log('Received:', message);
},
onCompleted(error) {
if (error) {
console.error('Stream failed:', error);
return;
}
console.log('Stream finished');
}
});
await ky.post('https://api.example.com/chat/stream', {
json: {prompt: 'Hello, world!'},
hooks: {afterResponse: [hook]},
timeout: false
});
Important: Set
timeout: falsewhen using SSE streams. Long-running streams will otherwise be interrupted by ky's default timeout.
createHook(options)Creates a ky AfterResponseHook that consumes the response body as an SSE stream.
import type {AfterResponseHook} from 'ky';
const hook: AfterResponseHook = createHook(options);
Behavior:
response.ok === false) or has no body, the hook returns immediately without consuming the stream.ReadableStream, decoded as UTF-8, and fed into the SSE parser.CreateHookOptions| Property | Type | Required | Description |
|---|---|---|---|
onData | (message: string) => void | Yes | Called for each data line. When a single SSE message contains multiple data: fields, they are split by \n and onData is called once per line. |
onCompleted | (error?: Error) => void | No | Called once when the stream ends. If the stream terminated due to an error (other than abort), the error argument is provided. Guaranteed to fire at most once. |
onAborted | () => void | No | Called when the request is aborted via AbortController. When aborted, onCompleted is not called. |
onEvent | (event: EventSourceMessage) => void | No | Called with the full EventSourceMessage for each SSE event that has a non-empty data field. Events with empty data are silently skipped. Fires before onData. |
onMessage | (event: EventSourceMessage) => void | No | Alias for onEvent. Both callbacks fire for the same event if both are provided. Events with empty data are also skipped. |
onReconnectInterval | (value: number) => void | No | Called when the SSE stream contains a retry: directive with the interval value in milliseconds. |
onData vs onEvent / onMessageonData receives the raw string content of each data: line. If a single SSE message has multiple data: fields, each line triggers a separate onData call.onEvent / onMessage receive the full EventSourceMessage object containing data, event, id, and retry fields. They fire once per SSE message, before onData.EventSourceMessageRe-exported from eventsource-parser. The shape is:
interface EventSourceMessage {
data: string;
event?: string;
id?: string;
}
Parse JSON-encoded SSE data and accumulate the response:
import ky from 'ky';
import {createHook} from '@bdky/ky-sse-hook';
interface ChatChunk {
answer: string;
is_end: boolean;
}
let fullResponse = '';
const hook = createHook({
onData(data) {
try {
const chunk: ChatChunk = JSON.parse(data);
fullResponse += chunk.answer;
console.log('Current response:', fullResponse);
}
catch {
console.error('Failed to parse chunk:', data);
}
},
onCompleted(error) {
if (error) {
console.error('Stream error:', error);
return;
}
console.log('Final response:', fullResponse);
}
});
await ky.post('https://api.example.com/chat/stream', {
json: {
messages: [
{role: 'user', content: 'Explain quantum computing'}
]
},
hooks: {afterResponse: [hook]},
timeout: false
});
Cancel a streaming request using AbortController:
import ky from 'ky';
import {createHook} from '@bdky/ky-sse-hook';
const controller = new AbortController();
const hook = createHook({
onData(data) {
console.log('Chunk:', data);
// Abort after receiving the first chunk
controller.abort();
},
onAborted() {
console.log('Request was aborted by user');
},
onCompleted(error) {
// Not called when aborted
if (error) {
console.error('Error:', error);
return;
}
console.log('Done');
}
});
await ky.post('https://api.example.com/chat/stream', {
json: {prompt: 'Tell me a long story'},
hooks: {afterResponse: [hook]},
signal: controller.signal,
timeout: false
});
Access the complete EventSourceMessage for each SSE event:
import ky from 'ky';
import {createHook} from '@bdky/ky-sse-hook';
const hook = createHook({
onData(data) {
console.log('Data:', data);
},
onEvent(event) {
console.log('Event type:', event.event);
console.log('Event ID:', event.id);
console.log('Event data:', event.data);
},
onReconnectInterval(interval) {
console.log('Server suggests retry interval:', interval, 'ms');
}
});
await ky.post('https://api.example.com/events', {
hooks: {afterResponse: [hook]},
timeout: false
});
Handle both stream-level and HTTP-level errors:
import ky, {HTTPError} from 'ky';
import {createHook} from '@bdky/ky-sse-hook';
const hook = createHook({
onData(data) {
console.log('Data:', data);
},
onCompleted(error) {
if (error) {
console.error('Stream reading failed:', error.message);
return;
}
console.log('Stream completed successfully');
}
});
try {
await ky.post('https://api.example.com/stream', {
json: {prompt: 'Hello'},
hooks: {afterResponse: [hook]},
timeout: false
});
}
catch (error) {
// ky throws HTTPError for non-2xx responses
// The hook skips non-OK responses, so HTTP errors
// are handled here, not in onCompleted
if (error instanceof HTTPError) {
console.error('HTTP error:', error.response.status);
}
}
ky.post(url, { hooks: { afterResponse: [hook] } })
│
▼
response.ok && response.body?
│ no → return (skip)
│ yes
▼
response.body.getReader()
│
▼
TextDecoder (UTF-8)
│
▼
eventsource-parser
│
├─ onEvent → options.onEvent()
│ options.onMessage()
│ data.split('\n') → options.onData() per line
│
└─ onRetry → options.onReconnectInterval()
│
▼
Stream done?
├─ yes → onCompleted()
├─ aborted → onAborted()
└─ error → onCompleted(error)
ReadableStream reader consumes the body in chunks.Uint8Array to string via TextDecoder.eventsource-parser, which emits structured SSE events.onCompleted() is called with no arguments.AbortController, onAborted() is called instead.onCompleted(error) is called.| Browser | Minimum Version |
|---|---|
| Chrome | >= 74 |
| Firefox | >= 90 |
| Safari | >= 14.1 |
| Edge | >= 79 |
| iOS Safari | >= 14.1 |
| Android Chrome | >= 74 |
Requires ReadableStream, TextDecoder, and fetch API support.
By design, stream-reading errors are passed to onCompleted(error) instead of being thrown. This allows the caller to handle errors in a callback style that's consistent with the rest of the API, and avoids unhandled promise rejections in streaming scenarios.
Yes. The hook works with any HTTP method (GET, POST, PUT, etc.) as long as the response returns an SSE body. It plugs into ky's afterResponse hook, which fires regardless of the request method.
onEvent and onMessage exist?They are convenience aliases. Both receive the same EventSourceMessage and both fire for every SSE event. This allows you to choose whichever name feels more natural in your codebase. If both are provided, both will be called.
The hook checks response.ok and skips processing if the response is not successful. Non-2xx errors are handled by ky's built-in error handling (e.g., HTTPError), so you can catch them with a standard try/catch around the ky call.
No. This hook processes a single SSE response. If the server sends a retry: directive, the value is forwarded to onReconnectInterval, but reconnection logic is left to the consumer. For auto-reconnect behavior, implement retry logic at the application level.
timeout: false?SSE streams are long-running connections. ky's default timeout (10 seconds) will abort the request before the stream completes. Set timeout: false to disable the timeout for streaming requests:
await ky.post(url, {
hooks: {afterResponse: [hook]},
timeout: false
});
MIT
FAQs
ky afterResponse hook for processing SSE streams
We found that @bdky/ky-sse-hook demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 3 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
/Security News
A mini Shai-Hulud campaign compromised Red Hat Cloud Services npm packages to steal developer and CI/CD secrets during installation.

Research
/Security News
The North Korean malware loader hides in a Packagist-listed package and its GitHub branch to fetch and execute remote code in a likely Contagious Interview-style lure.

Security News
The Rust project is moving toward formal rules on LLM use in contributions after months of internal debate over maintainer burden, code quality, and contributor experience.