Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion src/adapter/bun/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,28 @@ export const BunAdapter: ElysiaAdapter = {
? 'c.headers=c.request.headers.toJSON()\n'
: 'c.headers={}\n' +
'for(const [k,v] of c.request.headers.entries())' +
'c.headers[k]=v\n'
'c.headers[k]=v\n',
specializedResponse(kind, r, hasSet, saveResponse) {
// Bun's Response.json() is faster than new Response(JSON.stringify())
switch (kind) {
case 'Object':
case 'Array':
return hasSet
? `Response.json(${saveResponse}${r},c.set)`
: `Response.json(${saveResponse}${r})`
case 'String':
return hasSet
? `new Response(${saveResponse}${r},c.set)`
: `new Response(${saveResponse}${r})`
case 'Number':
case 'Boolean':
return hasSet
? `new Response(${saveResponse}''+${r},c.set)`
: `new Response(${saveResponse}''+${r})`
default:
return undefined
}
}
},
listen(app) {
return (options, callback) => {
Expand Down
19 changes: 19 additions & 0 deletions src/adapter/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,25 @@ export interface ElysiaAdapter {
declare?: string
}
>
/**
* Generate specialized inline response code for a known schema type kind.
*
* When the response schema type is known at compile time (e.g. Object, String),
* this generates optimized inline code that bypasses the generic mapResponse
* dispatch chain, eliminating 10+ type checks on the hot path.
*
* @param kind - The TypeBox schema Kind ('Object', 'Array', 'String', 'Number', 'Boolean')
* @param r - Variable name holding the response value
* @param hasSet - Whether set (headers/status/cookie) is active
* @param saveResponse - Code prefix for saving response to context
* @returns Generated fnLiteral string, or undefined to fall back to generic
*/
specializedResponse?: (
kind: string,
r: string,
hasSet: boolean,
saveResponse: string
) => string | undefined
}
composeGeneralHandler: {
parameters?: string
Expand Down
20 changes: 20 additions & 0 deletions src/adapter/web-standard/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,26 @@ export const WebStandardAdapter: ElysiaAdapter = {
composeHandler: {
mapResponseContext: 'c.request',
preferWebstandardHeaders: true,
specializedResponse(kind, r, hasSet, saveResponse) {
switch (kind) {
case 'Object':
case 'Array':
return hasSet
? `(c.set.headers['content-type']||(c.set.headers['content-type']='application/json'),new Response(JSON.stringify(${saveResponse}${r}),c.set))`
: `new Response(JSON.stringify(${saveResponse}${r}),{headers:{'content-type':'application/json'}})`
case 'String':
return hasSet
? `(c.set.headers['content-type']||(c.set.headers['content-type']='text/plain'),new Response(${saveResponse}${r},c.set))`
: `new Response(${saveResponse}${r})`
case 'Number':
case 'Boolean':
return hasSet
? `new Response(${saveResponse}''+${r},c.set)`
: `new Response(${saveResponse}''+${r})`
Comment on lines +33 to +37
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing content-type header for Number/Boolean when hasSet is true.

The String case (lines 30-31) sets content-type: text/plain when hasSet is true, but Number/Boolean does not. When c.set is passed as the Response init, the default content-type behavior is overridden—if c.set.headers lacks a content-type, the response will have none.

This inconsistency could cause clients to misinterpret numeric or boolean responses when headers/cookies/status are active.

🐛 Proposed fix to add content-type for Number/Boolean
 case 'Number':
 case 'Boolean':
     return hasSet
-        ? `new Response(${saveResponse}''+${r},c.set)`
+        ? `(c.set.headers['content-type']||(c.set.headers['content-type']='text/plain'),new Response(${saveResponse}''+${r},c.set))`
         : `new Response(${saveResponse}''+${r})`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/adapter/web-standard/index.ts` around lines 33 - 37, The Number/Boolean
branch returns a Response with c.set as init but doesn't ensure a content-type
when hasSet is true; update the 'Number' and 'Boolean' case in the switch (the
code that currently returns `new Response(${saveResponse}''+${r},c.set)` when
hasSet) to merge or override c.set.headers to include 'content-type':
'text/plain' (same as the String branch) before passing it to new Response, so
that when hasSet is true the Response init always contains a text/plain
content-type; reference the variables hasSet, saveResponse, r, c.set, and the
Response constructor in your change.

default:
return undefined
}
},
// @ts-ignore Bun specific
headers:
'c.headers={}\n' +
Expand Down
93 changes: 93 additions & 0 deletions src/compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,50 @@ import { coercePrimitiveRoot } from './replace-schema'
const allocateIf = (value: string, condition: unknown) =>
condition ? value : ''

/**
* Detect the TypeBox schema Kind for single-status-code response schemas.
*
* Used for specialized response code generation during AOT compilation.
* When the response type is known at compile time, we can generate inline
* code that bypasses the generic mapResponse dispatch chain (10+ type checks),
* leveraging:
* - Monomorphic inline caches (V8/JSC optimize single-type call sites)
* - Reduced branch misprediction (fewer conditional branches on hot path)
* - Partial evaluation (specialize code for known input types)
*
* Returns null for unsupported schemas (Union, Intersect, multiple status codes,
* Standard Schema providers), falling back to the generic path.
*/
const getResponseSchemaKind = (
validator: SchemaValidator
): string | null => {
if (!validator.response) return null

const keys = Object.keys(validator.response)
if (keys.length !== 1) return null

const check = validator.response[keys[0]]
if (check.provider === 'standard') return null

const schema = check.schema
if (!schema?.[Kind]) return null

switch (schema[Kind]) {
case 'Object':
return 'Object'
case 'String':
return 'String'
case 'Number':
return 'Number'
case 'Boolean':
return 'Boolean'
case 'Array':
return 'Array'
default:
return null
}
}

const defaultParsers = [
'json',
'text',
Expand Down Expand Up @@ -880,13 +924,62 @@ export const composeHandler = ({
return (_afterResponse = afterResponse)
}

const responseKind = getResponseSchemaKind(validator)
const canSpecialize =
responseKind !== null &&
!maybeStream &&
!hooks.mapResponse?.length &&
adapter.specializedResponse

const mapResponse = (r = 'r') => {
const after = afterResponse()
// When maybeStream is true, mapResponse may return a Promise (from handleStream)
// that can reject if the generator throws. We need to await it so the try-catch
// can properly catch the rejection and route it to error handling.
// Only add await if the function is async (maybeAsync), otherwise it would be a syntax error.
const awaitStream = maybeStream && maybeAsync ? 'await ' : ''

if (canSpecialize) {
const fast = adapter.specializedResponse!(
responseKind!,
r,
hasSet,
saveResponse
)

if (fast) {
const fallback = `${awaitStream}${hasSet ? 'mapResponse' : 'mapCompactResponse'}(${saveResponse}${r}${hasSet ? ',c.set' : ''}${mapResponseContext})`

let guard: string
switch (responseKind) {
case 'Object':
guard = `${r}!==null&&${r}!==undefined&&${r}.constructor===Object`
break
case 'Array':
guard = `Array.isArray(${r})`
break
case 'String':
guard = `typeof ${r}==='string'`
break
case 'Number':
guard = `typeof ${r}==='number'`
break
case 'Boolean':
guard = `typeof ${r}==='boolean'`
break
default:
guard = ''
}

if (guard) {
const response = `(${guard}?${fast}:${fallback})`

if (!after) return `return ${response}\n`
return `const _res=${response}\n` + after + `return _res`
}
}
}

const response = `${awaitStream}${hasSet ? 'mapResponse' : 'mapCompactResponse'}(${saveResponse}${r}${hasSet ? ',c.set' : ''}${mapResponseContext})\n`

if (!after) return `return ${response}`
Expand Down