fix(zod-openapi): don't validate the body if content-type is mismatched (#686)
* fix(zod-openapi): don't validate the body if content-type is mismatched * changesetpull/688/head
parent
5facd8cd25
commit
a6ec008fbd
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@hono/zod-openapi': patch
|
||||
---
|
||||
|
||||
fix: don't validate the body if content-type is mismatched
|
Binary file not shown.
|
@ -42,7 +42,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20240117.0",
|
||||
"hono": "^4.3.6",
|
||||
"hono": "^4.5.4",
|
||||
"jest": "^29.7.0",
|
||||
"tsup": "^8.0.1",
|
||||
"typescript": "^5.4.4",
|
||||
|
|
|
@ -54,12 +54,12 @@ type RequestTypes = {
|
|||
}
|
||||
|
||||
type IsJson<T> = T extends string
|
||||
? T extends `application/${infer Start}json${infer _End}`
|
||||
? Start extends '' | `${string}+` | `vnd.${string}+`
|
||||
? 'json'
|
||||
: never
|
||||
: never
|
||||
? T extends `application/${infer Start}json${infer _End}`
|
||||
? Start extends '' | `${string}+` | `vnd.${string}+`
|
||||
? 'json'
|
||||
: never
|
||||
: never
|
||||
: never
|
||||
|
||||
type IsForm<T> = T extends string
|
||||
? T extends
|
||||
|
@ -259,6 +259,17 @@ export type OpenAPIObjectConfigure<E extends Env, P extends string> =
|
|||
| OpenAPIObjectConfig
|
||||
| ((context: Context<E, P>) => OpenAPIObjectConfig)
|
||||
|
||||
const isJSONContentType = (contentType: string) => {
|
||||
return /^application\/([a-z-\.]+\+)?json/.test(contentType)
|
||||
}
|
||||
|
||||
const isFormContentType = (contentType: string) => {
|
||||
return (
|
||||
contentType.startsWith('multipart/form-data') ||
|
||||
contentType.startsWith('application/x-www-form-urlencoded')
|
||||
)
|
||||
}
|
||||
|
||||
export class OpenAPIHono<
|
||||
E extends Env = Env,
|
||||
S extends Schema = {},
|
||||
|
@ -390,16 +401,31 @@ export class OpenAPIHono<
|
|||
if (!(schema instanceof ZodType)) {
|
||||
continue
|
||||
}
|
||||
if (/^application\/([a-z-\.]+\+)?json/.test(mediaType)) {
|
||||
if (isJSONContentType(mediaType)) {
|
||||
const validator = zValidator('json', schema, hook as any)
|
||||
validators.push(validator as any)
|
||||
const mw: MiddlewareHandler = async (c, next) => {
|
||||
if (c.req.header('content-type')) {
|
||||
if (isJSONContentType(c.req.header('content-type')!)) {
|
||||
return await validator(c, next)
|
||||
}
|
||||
}
|
||||
c.req.addValidatedData('json', {})
|
||||
await next()
|
||||
}
|
||||
validators.push(mw)
|
||||
}
|
||||
if (
|
||||
mediaType.startsWith('multipart/form-data') ||
|
||||
mediaType.startsWith('application/x-www-form-urlencoded')
|
||||
) {
|
||||
if (isFormContentType(mediaType)) {
|
||||
const validator = zValidator('form', schema, hook as any)
|
||||
validators.push(validator as any)
|
||||
const mw: MiddlewareHandler = async (c, next) => {
|
||||
if (c.req.header('content-type')) {
|
||||
if (isFormContentType(c.req.header('content-type')!)) {
|
||||
return await validator(c, next)
|
||||
}
|
||||
}
|
||||
c.req.addValidatedData('form', {})
|
||||
await next()
|
||||
}
|
||||
validators.push(mw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -228,7 +228,7 @@ describe('coerce', () => {
|
|||
type Actual = ExtractSchema<typeof routes>['/api/users/:id']['$get']['input']
|
||||
type Expected = {
|
||||
param: {
|
||||
id: string | undefined
|
||||
id: string
|
||||
}
|
||||
}
|
||||
type verify = Expect<Equal<Expected, Actual>>
|
||||
|
|
|
@ -2,7 +2,7 @@ import type { RouteConfig } from '@asteasolutions/zod-to-openapi'
|
|||
import type { Context, TypedResponse } from 'hono'
|
||||
import { bearerAuth } from 'hono/bearer-auth'
|
||||
import { hc } from 'hono/client'
|
||||
import { describe, expect, expectTypeOf, it } from 'vitest'
|
||||
import { describe, expect, expectTypeOf, it, vi } from 'vitest'
|
||||
import type { RouteConfigToTypedResponse } from '../src/index'
|
||||
import { OpenAPIHono, createRoute, z } from '../src/index'
|
||||
import type { Equal, Expect } from 'hono/utils/types'
|
||||
|
@ -450,7 +450,7 @@ describe('JSON', () => {
|
|||
const app = new OpenAPIHono()
|
||||
|
||||
app.openapi(route, (c) => {
|
||||
const {id, title} = c.req.valid('json')
|
||||
const { id, title } = c.req.valid('json')
|
||||
return c.json({
|
||||
id,
|
||||
title,
|
||||
|
@ -489,6 +489,14 @@ describe('JSON', () => {
|
|||
const res = await app.request(req)
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
|
||||
it('Should return 200 response without a content-type', async () => {
|
||||
const req = new Request('http://localhost/posts', {
|
||||
method: 'POST',
|
||||
})
|
||||
const res = await app.request(req)
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Content-Type application/vnd.api+json', () => {
|
||||
|
@ -519,7 +527,7 @@ describe('JSON', () => {
|
|||
const app = new OpenAPIHono()
|
||||
|
||||
app.openapi(route, (c) => {
|
||||
const {id, title} = c.req.valid('json')
|
||||
const { id, title } = c.req.valid('json')
|
||||
return c.json({
|
||||
id,
|
||||
title,
|
||||
|
@ -641,12 +649,76 @@ describe('Form', () => {
|
|||
})
|
||||
})
|
||||
|
||||
it('Should return 400 response with correct contents', async () => {
|
||||
it('Should return 200 response without a content-type', async () => {
|
||||
const req = new Request('http://localhost/posts', {
|
||||
method: 'POST',
|
||||
})
|
||||
const res = await app.request(req)
|
||||
expect(res.status).toBe(400)
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
})
|
||||
|
||||
describe('JSON and Form', () => {
|
||||
const functionInForm = vi.fn()
|
||||
const functionInJSON = vi.fn()
|
||||
const route = createRoute({
|
||||
method: 'post',
|
||||
path: '/hello',
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
'application/x-www-form-urlencoded': {
|
||||
schema: z.custom(() => {
|
||||
functionInForm()
|
||||
return true
|
||||
}),
|
||||
},
|
||||
'application/json': {
|
||||
schema: z.custom(() => {
|
||||
functionInJSON()
|
||||
return true
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: 'response',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const app = new OpenAPIHono()
|
||||
|
||||
app.openapi(route, (c) => {
|
||||
return c.json(0)
|
||||
})
|
||||
|
||||
test('functionInJSON should not be called when the body is Form', async () => {
|
||||
const form = new FormData()
|
||||
form.append('foo', 'foo')
|
||||
await app.request('/hello', {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
})
|
||||
expect(functionInForm).toHaveBeenCalled()
|
||||
expect(functionInJSON).not.toHaveBeenCalled()
|
||||
functionInForm.mockReset()
|
||||
functionInJSON.mockReset()
|
||||
})
|
||||
test('functionInForm should not be called when the body is JSON', async () => {
|
||||
await app.request('/hello', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ foo: 'foo' }),
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
})
|
||||
expect(functionInForm).not.toHaveBeenCalled()
|
||||
expect(functionInJSON).toHaveBeenCalled()
|
||||
functionInForm.mockReset()
|
||||
functionInJSON.mockReset()
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -594,7 +594,7 @@ __metadata:
|
|||
"@asteasolutions/zod-to-openapi": "npm:^7.1.0"
|
||||
"@cloudflare/workers-types": "npm:^4.20240117.0"
|
||||
"@hono/zod-validator": "npm:0.2.2"
|
||||
hono: "npm:^4.3.6"
|
||||
hono: "npm:^4.5.4"
|
||||
jest: "npm:^29.7.0"
|
||||
tsup: "npm:^8.0.1"
|
||||
typescript: "npm:^5.4.4"
|
||||
|
@ -2379,10 +2379,10 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hono@npm:^4.3.6":
|
||||
version: 4.4.6
|
||||
resolution: "hono@npm:4.4.6"
|
||||
checksum: 065318f3fe021320b59f3daddacf7d74bfc3303de55f415a999b6967a9f09714e136528bc86cc880a45633cd85dec5428e41e902b5e3a3809f3cd17204302668
|
||||
"hono@npm:^4.5.4":
|
||||
version: 4.5.4
|
||||
resolution: "hono@npm:4.5.4"
|
||||
checksum: 980accb9567fe5ba4533c1378d175a95a0eef0a1fa8a03ceb1f9296d88d4ee8ff0e1447f5e69eb56150a8b6cc66f4a663ec9d1c1135f005f44f61353b58e276f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
Loading…
Reference in New Issue