Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support upgrading requests from the adapter #90

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,18 @@
"@types/jest": "^29.5.3",
"@types/node": "^20.10.0",
"@types/supertest": "^2.0.12",
"@types/ws": "^8.5.10",
"@whatwg-node/fetch": "^0.9.14",
"eslint": "^8.55.0",
"hono": "^3.11.7",
"jest": "^29.6.1",
"np": "^7.7.0",
"publint": "^0.1.16",
"supertest": "^6.3.3",
"superwstest": "^2.0.3",
"ts-jest": "^29.1.1",
"tsup": "^7.2.0",
"typescript": "^5.3.2"
"typescript": "^5.3.2",
"ws": "^8.16.0"
}
}
29 changes: 26 additions & 3 deletions src/listener.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { IncomingMessage, ServerResponse, OutgoingHttpHeaders } from 'node:http'
import { ServerResponse } from 'node:http'
import type { IncomingMessage, OutgoingHttpHeaders } from 'node:http'
import type { Http2ServerRequest, Http2ServerResponse } from 'node:http2'
import type { Socket } from 'node:net'
import { newRequest } from './request'
import { cacheKey } from './response'
import type { FetchCallback } from './types'
Expand Down Expand Up @@ -56,6 +58,10 @@ const responseViaResponseObject = async (
if (res instanceof Promise) {
res = await res.catch(handleFetchError)
}

if (res.status === 101) {
return
}

try {
if (cacheKey in res) {
Expand Down Expand Up @@ -103,10 +109,13 @@ const responseViaResponseObject = async (
}
}

type RequestListener = ReturnType<typeof getRequestListener>;

export const getRequestListener = (fetchCallback: FetchCallback) => {
return (
incoming: IncomingMessage | Http2ServerRequest,
outgoing: ServerResponse | Http2ServerResponse
outgoing: ServerResponse | Http2ServerResponse,
env?: Record<string, unknown>
) => {
let res

Expand All @@ -115,7 +124,11 @@ export const getRequestListener = (fetchCallback: FetchCallback) => {
const req = newRequest(incoming)

try {
res = fetchCallback(req) as Response | Promise<Response>
res = fetchCallback(req, env || {
incoming,
outgoing
}) as Response | Promise<Response>

if (cacheKey in res) {
// synchronous, cacheable response
return responseViaCache(res as Response, outgoing)
Expand All @@ -131,3 +144,13 @@ export const getRequestListener = (fetchCallback: FetchCallback) => {
return responseViaResponseObject(res, outgoing)
}
}

export const getUpgradeListener = (requestListener: RequestListener) => {
return (incoming: IncomingMessage, socket: Socket, head: Buffer) => {
const outgoing = new ServerResponse(incoming)
outgoing.assignSocket(socket)
requestListener(incoming, outgoing, {
incoming, outgoing, socket, head
})
}
}
6 changes: 5 additions & 1 deletion src/server.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { createServer as createServerHTTP } from 'node:http'
import type { AddressInfo } from 'node:net'
import { getRequestListener } from './listener'
import { getRequestListener, getUpgradeListener } from './listener'
import type { Options, ServerType } from './types'

export const createAdaptorServer = (options: Options): ServerType => {
const fetchCallback = options.fetch
const upgrade = options.upgrade !== false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would always add the upgrade listener unless it is actively set to false. Is this intended?

Copy link
Author

@Kyiro Kyiro Dec 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, idk if it should be the default or not so i made it the default

const requestListener = getRequestListener(fetchCallback)
// ts will complain about createServerHTTP and createServerHTTP2 not being callable, which works just fine
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const createServer: any = options.createServer || createServerHTTP
const server: ServerType = createServer(options.serverOptions || {}, requestListener)
if (upgrade) {
server.on('upgrade', getUpgradeListener(requestListener))
}
return server
}

Expand Down
5 changes: 4 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

import type { createServer, Server, ServerOptions as HttpServerOptions } from 'node:http'
import type {
createSecureServer as createSecureHttp2Server,
Expand All @@ -11,8 +12,9 @@ import type {
createServer as createHttpsServer,
ServerOptions as HttpsServerOptions,
} from 'node:https'
import type { Env } from 'hono'

export type FetchCallback = (request: Request) => Promise<unknown> | unknown
export type FetchCallback = (request: Request, env: Env) => Promise<unknown> | unknown

export type NextHandlerOption = {
fetch: FetchCallback
Expand Down Expand Up @@ -50,4 +52,5 @@ export type Options = {
fetch: FetchCallback
port?: number
hostname?: string
upgrade?: boolean
} & ServerOptions
40 changes: 39 additions & 1 deletion test/server.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import fs from 'node:fs'
import type { IncomingMessage } from 'node:http'
import { createServer as createHttp2Server } from 'node:http2'
import { createServer as createHTTPSServer } from 'node:https'
import type { Socket } from 'node:net'
import { Response as PonyfillResponse } from '@whatwg-node/fetch'
import { Hono } from 'hono'
import { basicAuth } from 'hono/basic-auth'
import { compress } from 'hono/compress'
import { poweredBy } from 'hono/powered-by'
import request from 'supertest'
import wsRequest from 'superwstest'
import { WebSocketServer } from 'ws'
import { createAdaptorServer } from '../src/server'

describe('Basic', () => {
Expand Down Expand Up @@ -469,7 +473,6 @@ describe('HTTP2', () => {
})

it('Should return 200 response - GET /', async () => {
// @ts-expect-error: @types/supertest is not updated yet
const res = await request(server, { http2: true }).get('/').trustLocalhost()
expect(res.status).toBe(200)
expect(res.headers['content-type']).toMatch(/text\/plain/)
Expand Down Expand Up @@ -518,3 +521,38 @@ describe('set child response to c.res', () => {
expect(res.headers['content-type']).toMatch(/application\/json/)
})
})

describe('upgrade websocket', () => {
const app = new Hono<{
Bindings: {
incoming: IncomingMessage;
socket: Socket;
head: Buffer;
}
}>()
const wsServer = new WebSocketServer({ noServer: true })

class UpgradeResponse extends Response {
status = 101
}

app.use('/', async (c) => {
wsServer.handleUpgrade(c.env.incoming, c.env.socket, c.env.head, (ws) => {
ws.send('Hello World!')
})

return new UpgradeResponse()
})

const server = createAdaptorServer(app)

// 'superwstest' needs the server to be listening
beforeEach((done) => { server.listen(0, 'localhost', done) })
afterEach((done) => { server.close(done) })

it('Should send a WebSocket message - GET /', async () => {
await wsRequest(server)
.ws('/')
.expectText('Hello World!')
})
})