Skip to content
Draft
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
122 changes: 122 additions & 0 deletions src/middleware/combine/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Hono } from '../../hono'
import type { MiddlewareHandler } from '../../types'
import { validator } from '../../validator'
import { every, except, some } from '.'

const nextMiddleware: MiddlewareHandler = async (_, next) => await next()
Expand Down Expand Up @@ -135,6 +136,50 @@ describe('some', () => {
})
})

describe('some with validators', () => {
const app = new Hono()
app.post(
'/',
some(
validator('json', (value) => value),
validator('form', (value) => value)
),
async (c) => {
const jsonData = c.req.valid('json')
const formData = c.req.valid('form')
return c.json({
json: jsonData,
form: formData,
})
}
)

it('Should validate a JSON request', async () => {
const res = await app.request('/', {
method: 'POST',
body: JSON.stringify({ foo: 'bar' }),
headers: {
'Content-Type': 'application/json',
},
})
expect(res.status).toBe(200)
const data = await res.json()
expect(data.json).toEqual({ foo: 'bar' })
})

it('Should validate a FormData request', async () => {
const form = new FormData()
form.append('foo', 'bar')
const res = await app.request('/', {
method: 'POST',
body: form,
})
expect(res.status).toBe(200)
const data = await res.json()
expect(data.form).toEqual({ foo: 'bar' })
})
})

describe('every', () => {
let app: Hono

Expand Down Expand Up @@ -226,6 +271,29 @@ describe('every', () => {
})
})

describe('every with validators', () => {
const app = new Hono()
app.post('/', every(validator('json', (value) => value)), async (c) => {
const jsonData = c.req.valid('json')
return c.json({
json: jsonData,
})
})

it('Should validate a JSON request', async () => {
const res = await app.request('/', {
method: 'POST',
body: JSON.stringify({ foo: 'bar' }),
headers: {
'Content-Type': 'application/json',
},
})
expect(res.status).toBe(200)
const data = await res.json()
expect(data.json).toEqual({ foo: 'bar' })
})
})

describe('except', () => {
let app: Hono

Expand Down Expand Up @@ -336,3 +404,57 @@ describe('except', () => {
expect(await res.text()).toBe('Hello Public User 123')
})
})

describe('except with validators', () => {
const app = new Hono()
app.post(
'/',
every(
except(
(c) => {
return c.req.query('body_type') !== 'json'
},
validator('json', (value) => value)
),
except(
(c) => {
return c.req.query('body_type') !== 'form'
},
validator('form', (value) => value)
)
),
async (c) => {
const jsonData = c.req.valid('json')
const formData = c.req.valid('form')
return c.json({
json: jsonData,
form: formData,
})
}
)

it('Should validate a JSON request', async () => {
const res = await app.request('/?body_type=json', {
method: 'POST',
body: JSON.stringify({ foo: 'bar' }),
headers: {
'Content-Type': 'application/json',
},
})
expect(res.status).toBe(200)
const data = await res.json()
expect(data.json).toEqual({ foo: 'bar' })
})

it('Should validate a FormData request', async () => {
const form = new FormData()
form.append('foo', 'bar')
const res = await app.request('/?body_type=form', {
method: 'POST',
body: form,
})
expect(res.status).toBe(200)
const data = await res.json()
expect(data.form).toEqual({ foo: 'bar' })
})
})
49 changes: 43 additions & 6 deletions src/middleware/combine/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
* @module
* Combine Middleware for Hono.
*/

/* eslint-disable @typescript-eslint/no-explicit-any */
import { compose } from '../../compose'
import type { Context } from '../../context'
import { METHOD_NAME_ALL } from '../../router'
import { TrieRouter } from '../../router/trie-router'
import type { MiddlewareHandler, Next } from '../../types'
import type { UnionToIntersection } from '../../utils/types'
import type { Input } from './../../types'

type Condition = (c: Context) => boolean

Expand Down Expand Up @@ -35,7 +37,19 @@ type Condition = (c: Context) => boolean
* ));
* ```
*/
export const some = (...middleware: (MiddlewareHandler | Condition)[]): MiddlewareHandler => {
export const some = <M extends (MiddlewareHandler | Condition)[]>(
...middleware: M
): MiddlewareHandler<
any,
any,
UnionToIntersection<
M[number] extends MiddlewareHandler<any, any, infer UInput> ? UInput : never
> extends infer UInputs
? UInputs extends Input
? UInputs
: never
: never
> => {
return async function some(c, next) {
let isNextCalled = false
const wrappedNext = () => {
Expand Down Expand Up @@ -96,7 +110,19 @@ export const some = (...middleware: (MiddlewareHandler | Condition)[]): Middlewa
* ));
* ```
*/
export const every = (...middleware: (MiddlewareHandler | Condition)[]): MiddlewareHandler => {
export const every = <M extends (MiddlewareHandler | Condition)[]>(
...middleware: M
): MiddlewareHandler<
any,
any,
UnionToIntersection<
M[number] extends MiddlewareHandler<any, any, infer UInput> ? UInput : never
> extends infer UInputs
? UInputs extends Input
? UInputs
: never
: never
> => {
return async function every(c, next) {
const currentRouteIndex = c.req.routeIndex
await compose(
Expand Down Expand Up @@ -138,10 +164,21 @@ export const every = (...middleware: (MiddlewareHandler | Condition)[]): Middlew
* ));
* ```
*/
export const except = (

export const except = <M extends MiddlewareHandler[]>(
condition: string | Condition | (string | Condition)[],
...middleware: MiddlewareHandler[]
): MiddlewareHandler => {
...middleware: M
): MiddlewareHandler<
any,
any,
UnionToIntersection<
M[number] extends MiddlewareHandler<any, any, infer UInput> ? UInput : never
> extends infer UInputs
? UInputs extends Input
? UInputs
: never
: never
> => {
let router: TrieRouter<true> | undefined = undefined
const conditions = (Array.isArray(condition) ? condition : [condition])
.map((condition) => {
Expand Down
57 changes: 46 additions & 11 deletions src/validator/validator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { ZodSchema } from 'zod'
import { z } from 'zod'
import { Hono } from '../hono'
import { HTTPException } from '../http-exception'
import { some } from '../middleware/combine'
import type {
ErrorHandler,
ExtractSchema,
Expand Down Expand Up @@ -120,20 +121,20 @@ describe('JSON', () => {
method: 'POST',
body: JSON.stringify({ foo: 'bar' }),
})
expect(res.status).toBe(200)
const data = await res.json()
expect(data.foo).toBeUndefined()
expect(res.status).toBe(415)
const data = await res.text()
expect(data).toBe('Missing Content-Type header "application/json"')
})

it('Should not validate if Content-Type is wrong', async () => {
it('Should not validate and return 415 if Content-Type is wrong', async () => {
const res = await app.request('http://localhost/post', {
method: 'POST',
headers: {
'Content-Type': 'text/plain;charset=utf-8',
},
body: JSON.stringify({ foo: 'bar' }),
})
expect(res.status).toBe(200)
expect(res.status).toBe(415)
})

it('Should validate if Content-Type is a application/json with a charset', async () => {
Expand Down Expand Up @@ -172,17 +173,17 @@ describe('JSON', () => {
expect(await res.json()).toEqual({ foo: 'bar' })
})

it('Should not validate if Content-Type does not start with application/json', async () => {
it('Should return 415 response if Content-Type does not start with application/json', async () => {
const res = await app.request('http://localhost/post', {
method: 'POST',
headers: {
'Content-Type': 'Xapplication/json',
},
body: JSON.stringify({ foo: 'bar' }),
})
expect(res.status).toBe(200)
const data = await res.json()
expect(data.foo).toBeUndefined()
expect(res.status).toBe(415)
const data = await res.text()
expect(data).toBe('Missing Content-Type header "application/json"')
})
})

Expand Down Expand Up @@ -231,6 +232,32 @@ describe('FormData', () => {
expect(await res.json()).toEqual({ message: 'hi' })
})

it('Should return 415 response if Content-Type is not set', async () => {
const formData = new FormData()
formData.append('message', 'hi')
const res = await app.request('http://localhost/post', {
method: 'POST',
headers: {
'Content-Type': '',
},
body: formData,
})
expect(res.status).toBe(415)
})

it('Should return 415 response if Content-Type is wrong', async () => {
const formData = new FormData()
formData.append('message', 'hi')
const res = await app.request('http://localhost/post', {
method: 'POST',
headers: {
'Content-Type': 'text/plain;charset=utf-8',
},
body: formData,
})
expect(res.status).toBe(415)
})

it('Should validate a URL Encoded Data', async () => {
const params = new URLSearchParams()
params.append('foo', 'bar')
Expand Down Expand Up @@ -323,8 +350,10 @@ describe('JSON and FormData', () => {
const app = new Hono()
app.post(
'/',
validator('json', (value) => value),
validator('form', (value) => value),
some(
validator('json', (value) => value),
validator('form', (value) => value)
),
async (c) => {
const jsonData = c.req.valid('json')
const formData = c.req.valid('form')
Expand Down Expand Up @@ -619,6 +648,9 @@ describe('Validator middleware with Zod validates Form data', () => {
it('Should validate Form data and return 400 response', async () => {
const res = await app.request('http://localhost/post', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
})
expect(res.status).toBe(400)
expect(await res.text()).toBe('Invalid!')
Expand Down Expand Up @@ -853,6 +885,9 @@ describe('Validator middleware with Zod multiple validators', () => {
it('Should validate both query param and form data and return 400 response', async () => {
const res = await app.request('http://localhost/posts?page=2', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
})
expect(res.status).toBe(400)
expect(await res.text()).toBe('Invalid!')
Expand Down
8 changes: 6 additions & 2 deletions src/validator/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ const jsonRegex = /^application\/([a-z-\.]+\+)?json(;\s*[a-zA-Z0-9\-]+\=([^;]+))
const multipartRegex = /^multipart\/form-data(;\s?boundary=[a-zA-Z0-9'"()+_,\-./:=?]+)?$/
const urlencodedRegex = /^application\/x-www-form-urlencoded(;\s*[a-zA-Z0-9\-]+\=([^;]+))*$/

const ERROR_MESSAGE_JSON = 'Missing Content-Type header "application/json"'
const ERROR_MESSAGE_FORM =
'Missing Content-Type header "multipart/form-data" or "application/x-www-form-urlencoded"'

export const validator = <
InputType,
P extends string,
Expand Down Expand Up @@ -72,7 +76,7 @@ export const validator = <
switch (target) {
case 'json':
if (!contentType || !jsonRegex.test(contentType)) {
break
throw new HTTPException(415, { message: ERROR_MESSAGE_JSON })
}
try {
value = await c.req.json()
Expand All @@ -86,7 +90,7 @@ export const validator = <
!contentType ||
!(multipartRegex.test(contentType) || urlencodedRegex.test(contentType))
) {
break
throw new HTTPException(415, { message: ERROR_MESSAGE_FORM })
}

let formData: FormData
Expand Down
Loading