Skip to content

Commit

Permalink
wip: compatibility layer
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 committed Mar 25, 2022
1 parent cdf9b7c commit 02182c3
Show file tree
Hide file tree
Showing 10 changed files with 114 additions and 96 deletions.
39 changes: 19 additions & 20 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { withoutTrailingSlash } from 'ufo'
import { lazyHandle, promisifyHandle } from './handle'
import { toEventHandler, createEvent } from './event'
import { toEventHandler, createEvent, H3CompatibilityEvent } from './event'
import { createError, sendError } from './error'
import { send, sendStream, isStream, MIMES } from './utils'
import type { IncomingMessage, ServerResponse } from './types/node'
Expand All @@ -25,7 +25,7 @@ export interface InputLayer {

export type InputStack = InputLayer[]

export type Matcher = (url: string, req?: IncomingMessage) => boolean
export type Matcher = (url: string, event?: H3CompatibilityEvent) => boolean

export interface AppUse {
(route: string | string [], handle: Middleware | Middleware[], options?: Partial<InputLayer>): App
Expand All @@ -44,7 +44,7 @@ export interface App {

export interface AppOptions {
debug?: boolean
onError?: (error: Error, req: IncomingMessage, res: ServerResponse) => any
onError?: (error: Error, event: H3CompatibilityEvent) => any
}

export function createApp (options: AppOptions = {}): App {
Expand All @@ -54,11 +54,12 @@ export function createApp (options: AppOptions = {}): App {

// @ts-ignore
const app: Partial<App> = function (req: IncomingMessage, res: ServerResponse) {
return _handle(req, res).catch((error: Error) => {
const event = createEvent(req, res)
return _handle(event).catch((error: Error) => {
if (options.onError) {
return options.onError(error, req, res)
return options.onError(error, event)
}
return sendError(res, error, !!options.debug)
return sendError(event, error, !!options.debug)
})
}

Expand Down Expand Up @@ -91,46 +92,44 @@ export function use (
return app
}

export function createHandle (stack: Stack, options: AppOptions): PHandle {
export function createHandle (stack: Stack, options: AppOptions) {
const spacing = options.debug ? 2 : undefined
return async function handle (req: IncomingMessage, res: ServerResponse) {
const event = createEvent(req, res)

return async function handle (event: H3CompatibilityEvent) {
// @ts-ignore express/connect compatibility
req.originalUrl = req.originalUrl || req.url || '/'
const reqUrl = req.url || '/'
const reqUrl = event.req.url || '/'
for (const layer of stack) {
if (layer.route.length > 1) {
if (!reqUrl.startsWith(layer.route)) {
continue
}
req.url = reqUrl.slice(layer.route.length) || '/'
event.req.url = reqUrl.slice(layer.route.length) || '/'
} else {
req.url = reqUrl
event.req.url = reqUrl
}
if (layer.match && !layer.match(req.url as string, req)) {
if (layer.match && !layer.match(event.req.url as string, event)) {
continue
}
const val = await layer.handler(event)
if (res.writableEnded) {
if (event.res.writableEnded) {
return
}
const type = typeof val
if (type === 'string') {
return send(res, val, MIMES.html)
return send(event.res, val, MIMES.html)
} else if (isStream(val)) {
return sendStream(res, val)
return sendStream(event.res, val)
} else if (type === 'object' || type === 'boolean' || type === 'number' /* IS_JSON */) {
if (val && (val as Buffer).buffer) {
return send(res, val)
return send(event.res, val)
} else if (val instanceof Error) {
throw createError(val)
} else {
return send(res, JSON.stringify(val, null, spacing), MIMES.json)
return send(event.res, JSON.stringify(val, null, spacing), MIMES.json)
}
}
}
if (!res.writableEnded) {
if (!event.res.writableEnded) {
throw createError({ statusCode: 404, statusMessage: 'Not Found' })
}
}
Expand Down
20 changes: 10 additions & 10 deletions src/error.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ServerResponse } from './types/node'
import type { H3CompatibilityEvent } from './event'
import { MIMES } from './utils'

/**
Expand Down Expand Up @@ -50,12 +50,12 @@ export function createError (input: Partial<H3Error>): H3Error {
* H3 internally uses this function to handle unhandled errors.<br>
* Note that calling this function will close the connection and no other data will be sent to client afterwards.
*
* @param res {ServerResponse} The ServerResponse object is passed as the second parameter in the handler function
@param event {H3CompatibilityEvent} H3 event or req passed by h3 handler
* @param error {H3Error|Error} Raised error
* @param debug {Boolean} Whether application is in debug mode.<br>
* In the debug mode the stack trace of errors will be return in response.
*/
export function sendError (res: ServerResponse, error: Error | H3Error, debug?: boolean) {
export function sendError (event: H3CompatibilityEvent, error: Error | H3Error, debug?: boolean) {
let h3Error: H3Error
if (error instanceof H3Error) {
h3Error = error
Expand All @@ -64,16 +64,16 @@ export function sendError (res: ServerResponse, error: Error | H3Error, debug?:
h3Error = createError(error)
}

if (res.writableEnded) {
if (event.res.writableEnded) {
return
}

res.statusCode = h3Error.statusCode
res.statusMessage = h3Error.statusMessage
event.res.statusCode = h3Error.statusCode
event.res.statusMessage = h3Error.statusMessage

const responseBody = {
statusCode: res.statusCode,
statusMessage: res.statusMessage,
statusCode: event.res.statusCode,
statusMessage: event.res.statusMessage,
stack: [] as string[],
data: h3Error.data
}
Expand All @@ -82,6 +82,6 @@ export function sendError (res: ServerResponse, error: Error | H3Error, debug?:
responseBody.stack = (h3Error.stack || '').split('\n').map(l => l.trim())
}

res.setHeader('Content-Type', MIMES.json)
res.end(JSON.stringify(responseBody, null, 2))
event.res.setHeader('Content-Type', MIMES.json)
event.res.end(JSON.stringify(responseBody, null, 2))
}
20 changes: 14 additions & 6 deletions src/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,21 @@ import type { IncomingMessage, ServerResponse } from './types/node'
import type { Handle, Middleware } from './handle'

export interface H3Event {
event: H3Event
req: IncomingMessage
res: ServerResponse,
next?: (err?: Error) => void
}

export type H3CompatibilityEvent = H3Event | IncomingMessage | ServerResponse

export type _JSONValue<T=string|number|boolean> = T | T[] | Record<string, T>
export type JSONValue = _JSONValue<_JSONValue>
export type H3Response = void | JSONValue | Buffer

export interface H3EventHandler {
__is_handler__?: true
(event: H3Event): H3Response| Promise<H3Response>
(event: H3CompatibilityEvent): H3Response| Promise<H3Response>
}

export function defineEventHandler (handler: H3EventHandler) {
Expand All @@ -40,9 +43,14 @@ export function toEventHandler (handler: H3EventHandler | Handle | Middleware):
}
}

export function createEvent (req: IncomingMessage, res: ServerResponse): H3Event {
return {
req,
res
}
export function createEvent (req: IncomingMessage, res: ServerResponse): H3CompatibilityEvent {
const event = { req, res } as H3Event
// Backward comatibility
// Allow interchangable usage of {event,req,res}.*
event.event = event
event.req.event = event
event.req.res = res
event.res.event = event
event.res.req.event = event
return event
}
2 changes: 1 addition & 1 deletion src/handle.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { withoutTrailingSlash, withoutBase } from 'ufo'
import type { IncomingMessage, ServerResponse } from './types/node'
import type { IncomingMessage, ServerResponse } from './types'

export type Handle<T = any, ReqT={}> = (req: IncomingMessage & ReqT, res: ServerResponse) => T
export type PHandle = Handle<Promise<any>>
Expand Down
3 changes: 3 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './cookie'
export * from './http'
export * from './node'
16 changes: 15 additions & 1 deletion src/types/node.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
export type { IncomingMessage, ServerResponse } from 'http'
import type http from 'http'
import type { H3Event } from '../event'

export interface H3CompatbilityAugmentions {
event: H3Event,
req: H3Event['req'],
res: H3Event['res']
}

export interface IncomingMessage extends http.IncomingMessage, H3CompatbilityAugmentions {}
export interface ServerResponse extends http.ServerResponse, Omit<H3CompatbilityAugmentions, 'req'> {
req: http.ServerResponse['req'] & {
event: H3Event
}
}

export type Encoding = false | 'ascii' | 'utf8' | 'utf-8' | 'utf16le' | 'ucs2' | 'ucs-2' | 'base64' | 'latin1' | 'binary' | 'hex'
32 changes: 13 additions & 19 deletions src/utils/body.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,39 @@
import type { IncomingMessage } from 'http'
import destr from 'destr'
import type { Encoding } from '../types/node'
import type { HTTPMethod } from '../types/http'
import type { H3CompatibilityEvent } from '../event'
import { assertMethod } from './request'

const RawBodySymbol = Symbol('h3RawBody')
const ParsedBodySymbol = Symbol('h3RawBody')

const PayloadMethods = ['PATCH', 'POST', 'PUT', 'DELETE'] as HTTPMethod[]

interface _IncomingMessage extends IncomingMessage {
[RawBodySymbol]?: Promise<Buffer>
ParsedBodySymbol?: any
body?: any // unenv
}

/**
* Reads body of the request and returns encoded raw string (default) or `Buffer` if encoding if falsy.
* @param req {IncomingMessage} An IncomingMessage object is created by [http.Server](https://nodejs.org/api/http.html#http_class_http_server)
* @param event {H3CompatibilityEvent} H3 event or req passed by h3 handler
* @param encoding {Encoding} encoding="utf-8" - The character encoding to use.
*
* @return {String|Buffer} Encoded raw string or raw Buffer of the body
*/
export function useRawBody (req: _IncomingMessage, encoding: Encoding = 'utf-8'): Encoding extends false ? Buffer : Promise<string> {
export function useRawBody (event: H3CompatibilityEvent, encoding: Encoding = 'utf-8'): Encoding extends false ? Buffer : Promise<string> {
// Ensure using correct HTTP method before attempt to read payload
assertMethod(req, PayloadMethods)
assertMethod(event, PayloadMethods)

if (RawBodySymbol in req) {
const promise = Promise.resolve(req[RawBodySymbol])
if (RawBodySymbol in event.req) {
const promise = Promise.resolve((event.req as any)[RawBodySymbol])
// @ts-ignore
return encoding ? promise.then(buff => buff.toString(encoding)) : promise
}

// Workaround for unenv issue https://github.com/unjs/unenv/issues/8
if ('body' in req) {
return Promise.resolve(req.body)
if ('body' in event.req) {
return Promise.resolve((event.req as any).body)
}

const promise = req[RawBodySymbol] = new Promise<Buffer>((resolve, reject) => {
const promise = (event.req as any)[RawBodySymbol] = new Promise<Buffer>((resolve, reject) => {
const bodyData: any[] = []
req
event.req
.on('error', (err) => { reject(err) })
.on('data', (chunk) => { bodyData.push(chunk) })
.on('end', () => { resolve(Buffer.concat(bodyData)) })
Expand All @@ -51,7 +45,7 @@ export function useRawBody (req: _IncomingMessage, encoding: Encoding = 'utf-8')

/**
* Reads request body and try to safely parse using [destr](https://github.com/unjs/destr)
* @param req {IncomingMessage} An IncomingMessage object created by [http.Server](https://nodejs.org/api/http.html#http_class_http_server)
* @param event {H3CompatibilityEvent} H3 event or req passed by h3 handler
* @param encoding {Encoding} encoding="utf-8" - The character encoding to use.
*
* @return {*} The `Object`, `Array`, `String`, `Number`, `Boolean`, or `null` value corresponding to the request JSON body
Expand All @@ -60,14 +54,14 @@ export function useRawBody (req: _IncomingMessage, encoding: Encoding = 'utf-8')
* const body = await useBody(req)
* ```
*/
export async function useBody<T=any> (req: _IncomingMessage): Promise<T> {
export async function useBody<T=any> (event: H3CompatibilityEvent): Promise<T> {
// @ts-ignore
if (ParsedBodySymbol in req) {
// @ts-ignore
return req[ParsedBodySymbol]
}

const body = await useRawBody(req)
const body = await useRawBody(event)
const json = destr(body)

// @ts-ignore
Expand Down
26 changes: 13 additions & 13 deletions src/utils/cookie.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,59 @@
import type { IncomingMessage, ServerResponse } from 'http'
import { parse, serialize } from 'cookie-es'
import type { CookieSerializeOptions } from '../types/cookie'
import type { H3CompatibilityEvent } from '../event'
import { appendHeader } from './response'

/**
* Parse the request to get HTTP Cookie header string and returning an object of all cookie name-value pairs.
* @param req {IncomingMessage} An IncomingMessage object created by [http.Server](https://nodejs.org/api/http.html#http_class_http_server)
* @param event {H3CompatibilityEvent} H3 event or req passed by h3 handler
* @returns Object of cookie name-value pairs
* ```ts
* const cookies = useCookies(req)
* ```
*/
export function useCookies (req: IncomingMessage): Record<string, string> {
return parse(req.headers.cookie || '')
export function useCookies (event: H3CompatibilityEvent): Record<string, string> {
return parse(event.req.headers.cookie || '')
}

/**
* Get a cookie value by name.
* @param req {IncomingMessage} An IncomingMessage object created by [http.Server](https://nodejs.org/api/http.html#http_class_http_server)
* @param event {H3CompatibilityEvent} H3 event or req passed by h3 handler
* @param name Name of the cookie to get
* @returns {*} Value of the cookie (String or undefined)
* ```ts
* const authorization = useCookie(request, 'Authorization')
* ```
*/
export function useCookie (req: IncomingMessage, name: string): string | undefined {
return useCookies(req)[name]
export function useCookie (event: H3CompatibilityEvent, name: string): string | undefined {
return useCookies(event)[name]
}

/**
* Set a cookie value by name.
* @param res {ServerResponse} A ServerResponse object created by [http.Server](https://nodejs.org/api/http.html#http_class_http_server)
* @param event {H3CompatibilityEvent} H3 event or res passed by h3 handler
* @param name Name of the cookie to set
* @param value Value of the cookie to set
* @param serializeOptions {CookieSerializeOptions} Options for serializing the cookie
* ```ts
* setCookie(res, 'Authorization', '1234567')
* ```
*/
export function setCookie (res: ServerResponse, name: string, value: string, serializeOptions?: CookieSerializeOptions) {
export function setCookie (event: H3CompatibilityEvent, name: string, value: string, serializeOptions?: CookieSerializeOptions) {
const cookieStr = serialize(name, value, serializeOptions)
appendHeader(res, 'Set-Cookie', cookieStr)
appendHeader(event, 'Set-Cookie', cookieStr)
}

/**
* Set a cookie value by name.
* @param res {ServerResponse} A ServerResponse object created by [http.Server](https://nodejs.org/api/http.html#http_class_http_server)
* @param event {H3CompatibilityEvent} H3 event or res passed by h3 handler
* @param name Name of the cookie to delete
* @param serializeOptions {CookieSerializeOptions} Cookie options
* ```ts
* deleteCookie(res, 'SessionId')
* ```
*/
export function deleteCookie (res: ServerResponse, name: string, serializeOptions?: CookieSerializeOptions) {
setCookie(res, name, '', {
export function deleteCookie (event: H3CompatibilityEvent, name: string, serializeOptions?: CookieSerializeOptions) {
setCookie(event, name, '', {
...serializeOptions,
maxAge: 0
})
Expand Down
Loading

0 comments on commit 02182c3

Please sign in to comment.