fix(node-ws): adapter shouldn't send buffer as a event (#1094)

* fix(node-ws): adapter shouldn't send buffer as a event

* chore: changeset
pull/1095/head
Shotaro Nakamura 2025-04-01 21:35:45 +09:00 committed by GitHub
parent b18f24379b
commit 519404ad2c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 56 additions and 48 deletions

View File

@ -0,0 +1,5 @@
---
'@hono/node-ws': patch
---
Adapter won't send Buffer as a MessageEvent.

View File

@ -1,5 +1,5 @@
import type { Context } from 'hono' import type { Context } from 'hono'
import { getCookie } from 'hono/cookie'; import { getCookie } from 'hono/cookie'
import { createMiddleware } from 'hono/factory' import { createMiddleware } from 'hono/factory'
import { HTTPException } from 'hono/http-exception' import { HTTPException } from 'hono/http-exception'

View File

@ -52,9 +52,7 @@ describe('WebSocket helper', () => {
}) })
it('Should be rejected if upgradeWebSocket is not used', async () => { it('Should be rejected if upgradeWebSocket is not used', async () => {
app.get( app.get('/', (c) => c.body(''))
'/', (c)=>c.body('')
)
{ {
const ws = new WebSocket('ws://localhost:3030/') const ws = new WebSocket('ws://localhost:3030/')
@ -70,7 +68,8 @@ describe('WebSocket helper', () => {
expect(await mainPromise).toBe(true) expect(await mainPromise).toBe(true)
} }
{ //also should rejected on fallback {
//also should rejected on fallback
const ws = new WebSocket('ws://localhost:3030/notFound') const ws = new WebSocket('ws://localhost:3030/notFound')
const mainPromise = new Promise<boolean>((resolve) => { const mainPromise = new Promise<boolean>((resolve) => {
ws.onerror = () => { ws.onerror = () => {
@ -202,11 +201,11 @@ describe('WebSocket helper', () => {
ws.send(binaryData) ws.send(binaryData)
const receivedMessage = await mainPromise const receivedMessage = await mainPromise
expect(receivedMessage).toBeInstanceOf(Buffer) expect(receivedMessage).toBeInstanceOf(ArrayBuffer)
expect((receivedMessage as Buffer).byteLength).toBe(binaryData.length) expect((receivedMessage as ArrayBuffer).byteLength).toBe(binaryData.length)
binaryData.forEach((val, idx) => { binaryData.forEach((val, idx) => {
expect((receivedMessage as Buffer).at(idx)).toBe(val) expect(new Uint8Array(receivedMessage as ArrayBuffer)[idx]).toBe(val)
}) })
}) })

View File

@ -25,7 +25,10 @@ export interface NodeWebSocketInit {
*/ */
export const createNodeWebSocket = (init: NodeWebSocketInit): NodeWebSocket => { export const createNodeWebSocket = (init: NodeWebSocketInit): NodeWebSocket => {
const wss = new WebSocketServer({ noServer: true }) const wss = new WebSocketServer({ noServer: true })
const waiterMap = new Map<IncomingMessage, { resolve: (ws: WebSocket) => void, response: Response }>() const waiterMap = new Map<
IncomingMessage,
{ resolve: (ws: WebSocket) => void; response: Response }
>()
wss.on('connection', (ws, request) => { wss.on('connection', (ws, request) => {
const waiter = waiterMap.get(request) const waiter = waiterMap.get(request)
@ -64,9 +67,9 @@ export const createNodeWebSocket = (init: NodeWebSocketInit): NodeWebSocket => {
if (!waiter || waiter.response !== response) { if (!waiter || waiter.response !== response) {
socket.end( socket.end(
'HTTP/1.1 400 Bad Request\r\n' + 'HTTP/1.1 400 Bad Request\r\n' +
'Connection: close\r\n' + 'Connection: close\r\n' +
'Content-Length: 0\r\n' + 'Content-Length: 0\r\n' +
'\r\n' '\r\n'
) )
waiterMap.delete(request) waiterMap.delete(request)
return return
@ -113,7 +116,11 @@ export const createNodeWebSocket = (init: NodeWebSocketInit): NodeWebSocket => {
for (const data of datas) { for (const data of datas) {
events.onMessage?.( events.onMessage?.(
new MessageEvent('message', { new MessageEvent('message', {
data: isBinary ? data : data.toString('utf-8'), data: isBinary
? data instanceof ArrayBuffer
? data
: data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength)
: data.toString('utf-8'),
}), }),
ctx ctx
) )

View File

@ -77,13 +77,13 @@ export class AuthFlow {
const url = 'https://id.twitch.tv/oauth2/token' const url = 'https://id.twitch.tv/oauth2/token'
const response = (await fetch(url, { const response = await fetch(url, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
}, },
body: parsedOptions, body: parsedOptions,
}).then((res) => res.json() as Promise<TwitchTokenResponse>)) }).then((res) => res.json() as Promise<TwitchTokenResponse>)
if ('error' in response) { if ('error' in response) {
throw new HTTPException(400, { message: response.error }) throw new HTTPException(400, { message: response.error })

View File

@ -14,18 +14,18 @@ export async function refreshToken(
client_secret, client_secret,
}) })
const response = (await fetch('https://id.twitch.tv/oauth2/token', { const response = await fetch('https://id.twitch.tv/oauth2/token', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
}, },
body: params, body: params,
}).then((res) => res.json() as Promise<TwitchRefreshResponse>)) }).then((res) => res.json() as Promise<TwitchRefreshResponse>)
if ('error' in response) { if ('error' in response) {
throw new HTTPException(400, { message: response.error }) throw new HTTPException(400, { message: response.error })
} }
if ('message' in response) { if ('message' in response) {
throw new HTTPException(400, { message: response.message as string }) throw new HTTPException(400, { message: response.message as string })
} }

View File

@ -2,10 +2,7 @@ import { HTTPException } from 'hono/http-exception'
import { toQueryParams } from '../../utils/objectToQuery' import { toQueryParams } from '../../utils/objectToQuery'
import type { TwitchRevokingResponse } from './types' import type { TwitchRevokingResponse } from './types'
export async function revokeToken( export async function revokeToken(client_id: string, token: string): Promise<boolean> {
client_id: string,
token: string
): Promise<boolean> {
const params = toQueryParams({ const params = toQueryParams({
client_id: client_id, client_id: client_id,
token, token,
@ -23,14 +20,16 @@ export async function revokeToken(
if (!res.ok) { if (!res.ok) {
// Try to parse error response // Try to parse error response
try { try {
const errorResponse = await res.json() as TwitchRevokingResponse const errorResponse = (await res.json()) as TwitchRevokingResponse
if (errorResponse && typeof errorResponse === 'object' && 'message' in errorResponse) { if (errorResponse && typeof errorResponse === 'object' && 'message' in errorResponse) {
throw new HTTPException(400, { message: errorResponse.message }) throw new HTTPException(400, { message: errorResponse.message })
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) { } catch (e) {
// If parsing fails, throw a generic error with the status // If parsing fails, throw a generic error with the status
throw new HTTPException(400, { message: `Token revocation failed with status: ${res.status}` }) throw new HTTPException(400, {
message: `Token revocation failed with status: ${res.status}`,
})
} }
} }

View File

@ -2,10 +2,10 @@ export type Scopes =
// Analytics // Analytics
| 'analytics:read:extensions' | 'analytics:read:extensions'
| 'analytics:read:games' | 'analytics:read:games'
// Bits // Bits
| 'bits:read' | 'bits:read'
// Channel // Channel
| 'channel:bot' | 'channel:bot'
| 'channel:manage:ads' | 'channel:manage:ads'
@ -85,11 +85,11 @@ export type Scopes =
| 'moderator:read:vips' | 'moderator:read:vips'
| 'moderator:read:warnings' | 'moderator:read:warnings'
| 'moderator:manage:warnings' | 'moderator:manage:warnings'
// IRC Chat Scopes // IRC Chat Scopes
| 'chat:edit' | 'chat:edit'
| 'chat:read' | 'chat:read'
// PubSub-specific Chat Scopes // PubSub-specific Chat Scopes
| 'whispers:read' | 'whispers:read'
@ -110,7 +110,6 @@ export type TwitchRefreshError = Required<Pick<TwitchErrorResponse, 'status' | '
export type TwitchTokenError = Required<Pick<TwitchErrorResponse, 'status' | 'message' | 'error'>> export type TwitchTokenError = Required<Pick<TwitchErrorResponse, 'status' | 'message' | 'error'>>
// Success responses types from Twitch API // Success responses types from Twitch API
export interface TwitchValidateSuccess { export interface TwitchValidateSuccess {
client_id: string client_id: string
@ -150,19 +149,21 @@ export type TwitchTokenResponse = TwitchTokenSuccess | TwitchTokenError
export type TwitchValidateResponse = TwitchValidateSuccess | TwitchValidateError export type TwitchValidateResponse = TwitchValidateSuccess | TwitchValidateError
export interface TwitchUserResponse { export interface TwitchUserResponse {
data: [{ data: [
id: string {
login: string id: string
display_name: string login: string
type: string display_name: string
broadcaster_type: string type: string
description: string broadcaster_type: string
profile_image_url: string description: string
offline_image_url: string profile_image_url: string
view_count: number offline_image_url: string
email: string view_count: number
created_at: string email: string
}] created_at: string
}
]
} }
export type TwitchUser = TwitchUserResponse['data'][0] export type TwitchUser = TwitchUserResponse['data'][0]

View File

@ -1,14 +1,11 @@
import { HTTPException } from 'hono/http-exception' import { HTTPException } from 'hono/http-exception'
import type { TwitchValidateResponse } from './types' import type { TwitchValidateResponse } from './types'
export async function validateToken( export async function validateToken(token: string): Promise<TwitchValidateResponse> {
token: string
): Promise<TwitchValidateResponse> {
const response = await fetch('https://id.twitch.tv/oauth2/validate', { const response = await fetch('https://id.twitch.tv/oauth2/validate', {
method: 'GET', method: 'GET',
headers: { headers: {
authorization: `Bearer ${token}`, authorization: `Bearer ${token}`,
}, },
}).then((res) => res.json() as Promise<TwitchValidateResponse>) }).then((res) => res.json() as Promise<TwitchValidateResponse>)