Skip to content
Merged
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
48 changes: 47 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ app.get(
}))
)

const wss = new WebSocketServer({ noServer: true }) // important to create with `noServer: true`
const wss = new WebSocketServer({ noServer: true })
serve({
fetch: app.fetch,
websocket: { server: wss },
Expand Down Expand Up @@ -332,6 +332,52 @@ type Http2Bindings = {
}
```

## Early Hints Middleware

You can send HTTP 103 Early Hints to instruct browsers to preload or preconnect resources before the final response is prepared. The middleware is supported under Node.js bindings (HTTP/1.1 and HTTP/2).

### Usage

Import `earlyHints` from `@hono/node-server/early-hints`:

#### Static links

```ts
import { serve } from '@hono/node-server'
import { earlyHints } from '@hono/node-server/early-hints'
import { Hono } from 'hono'

const app = new Hono()

app.use(
earlyHints({
link: '</styles.css>; rel=preload; as=style',
})
)

app.get('/', (c) => {
return c.html('<!DOCTYPE html><html><body><h1>Hello Hono!</h1></body></html>')
})

serve(app)
```

#### Dynamic links

```ts
app.use(
earlyHints({
link: (c) =>
c.req.query('theme') === 'dark'
? '</dark.css>; rel=preload; as=style'
: '</light.css>; rel=preload; as=style',
})
)
```

> [!NOTE]
> Early Hints are sent only for requests that look like document navigations. If `Sec-Fetch-Mode` or `Sec-Fetch-Dest` is present with a value other than `navigate` or `document`, for example a `fetch()` or XHR call from a browser, a subresource request, or an iframe navigation, the middleware skips the hints and continues to the handler. Requests without these headers, such as `curl` or `fetch()` from a JavaScript runtime, are treated as navigations and do receive Early Hints.

## Direct response from Node.js API

You can directly respond to the client from the Node.js API.
Expand Down
13 changes: 13 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@
"types": "./dist/conninfo.d.cts",
"default": "./dist/conninfo.cjs"
}
},
"./early-hints": {
"import": {
"types": "./dist/early-hints.d.mts",
"default": "./dist/early-hints.mjs"
},
"require": {
"types": "./dist/early-hints.d.cts",
"default": "./dist/early-hints.cjs"
}
}
},
"typesVersions": {
Expand All @@ -63,6 +73,9 @@
],
"conninfo": [
"./dist/conninfo.d.mts"
],
"early-hints": [
"./dist/early-hints.d.mts"
]
}
},
Expand Down
55 changes: 55 additions & 0 deletions src/early-hints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { Context, Env, MiddlewareHandler } from 'hono'
import type { HttpBindings } from './types'

export type EarlyHintsOptions<E extends Env = Env> = {
link: string | string[] | ((c: Context<E>) => string | string[] | undefined)
}

/**
* Early Hints middleware for Node.js
* Automatically sends a 103 Early Hints informational response with the specified Link header(s).
*
* @param options EarlyHintsOptions
* @returns MiddlewareHandler
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const earlyHints = <E extends Env = any>(
options: EarlyHintsOptions<E>
): MiddlewareHandler<E> => {
let warned = false

return async (c, next) => {
const mode = c.req.header('Sec-Fetch-Mode')
const dest = c.req.header('Sec-Fetch-Dest')

if ((mode && mode !== 'navigate') || (dest && dest !== 'document')) {
return next()
}

const env = c.env || {}
const bindings = (env.server ? env.server : env) as HttpBindings
const outgoing = bindings?.outgoing

// Capability check: outgoing.writeEarlyHints exists and is a function.
// This guard exists for non-Node runtimes and non-HTTP bindings.
if (typeof outgoing?.writeEarlyHints !== 'function') {
if (!warned) {
console.warn(
'Early Hints Middleware is not supported because writeEarlyHints is not defined.'
)
warned = true
}
return await next()
}

if (!outgoing.headersSent) {
const link = typeof options.link === 'function' ? options.link(c) : options.link

if (link !== undefined && (Array.isArray(link) ? link.length > 0 : Boolean(link))) {
outgoing.writeEarlyHints({ link })
}
}

await next()
}
}
Loading