Skip to content

Commit

Permalink
feat(middleware): enable to pass ...c.req to init options
Browse files Browse the repository at this point in the history
  • Loading branch information
usualoma committed Jan 26, 2025
1 parent 3a5ef0f commit b6eb510
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 92 deletions.
81 changes: 20 additions & 61 deletions src/helper/proxy/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { proxyFetch } from '.'
describe('Proxy Middleware', () => {
describe('proxyFetch', () => {
beforeEach(() => {
global.fetch = vi.fn().mockImplementation((req) => {
global.fetch = vi.fn().mockImplementation(async (req) => {
if (req.url === 'https://example.com/compressed') {
return Promise.resolve(
new Response('ok', {
Expand All @@ -26,6 +26,8 @@ describe('Proxy Middleware', () => {
},
})
)
} else if (req.url === 'https://example.com/post' && req.method === 'POST') {
return Promise.resolve(new Response(`request body: ${await req.text()}`))
}
return Promise.resolve(new Response('not found', { status: 404 }))
})
Expand Down Expand Up @@ -82,71 +84,28 @@ describe('Proxy Middleware', () => {
expect(res.headers.get('Content-Range')).toBe('bytes 0-2/1024')
})

it('proxySetRequestHeaders option', async () => {
it('POST request', async () => {
const app = new Hono()
app.get('/proxy/:path', (c) =>
proxyFetch(
new Request(`https://example.com/${c.req.param('path')}`, {
headers: {
'X-Request-Id': '123',
'X-To-Be-Deleted': 'to-be-deleted',
'Accept-Encoding': 'gzip',
},
}),
{
proxySetRequestHeaders: {
'X-Request-Id': 'abc',
'X-Forwarded-For': '127.0.0.1',
'X-Forwarded-Host': 'example.com',
'X-To-Be-Deleted': undefined,
},
}
)
)
const res = await app.request('/proxy/compressed')
const req = (global.fetch as ReturnType<typeof vi.fn>).mock.calls[0][0]

expect(req.url).toBe('https://example.com/compressed')
expect(req.headers.get('X-Request-Id')).toBe('abc')
expect(req.headers.get('X-Forwarded-For')).toBe('127.0.0.1')
expect(req.headers.get('X-Forwarded-Host')).toBe('example.com')
expect(req.headers.get('X-To-Be-Deleted')).toBeNull()
expect(req.headers.get('Accept-Encoding')).toBeNull()

expect(res.status).toBe(200)
expect(res.headers.get('X-Response-Id')).toBe('456')
expect(res.headers.get('Content-Encoding')).toBeNull()
expect(res.headers.get('Content-Length')).toBeNull()
expect(res.headers.get('Content-Range')).toBe('bytes 0-2/1024')
})

it('proxySetRequestHeaderNames option', async () => {
const app = new Hono()
app.get('/proxy/:path', (c) =>
proxyFetch(
new Request(`https://example.com/${c.req.param('path')}`, {
headers: {
'X-Request-Id': '123',
'Accept-Encoding': 'gzip',
},
}),
{
proxyDeleteResponseHeaderNames: ['X-Response-Id'],
}
)
)
const res = await app.request('/proxy/compressed')
app.all('/proxy/:path', (c) => {
return proxyFetch(`https://example.com/${c.req.param('path')}`, {
...c.req,
headers: {
...c.req.header(),
'X-Request-Id': '123',
'Accept-Encoding': 'gzip',
},
})
})
const res = await app.request('/proxy/post', {
method: 'POST',
body: 'test',
})
const req = (global.fetch as ReturnType<typeof vi.fn>).mock.calls[0][0]

expect(req.url).toBe('https://example.com/compressed')
expect(req.headers.get('X-Request-Id')).toBe('123')
expect(req.headers.get('Accept-Encoding')).toBeNull()
expect(req.url).toBe('https://example.com/post')

expect(res.status).toBe(200)
expect(res.headers.get('X-Response-Id')).toBeNull()
expect(res.headers.get('Content-Encoding')).toBeNull()
expect(res.headers.get('Content-Length')).toBeNull()
expect(res.headers.get('Content-Range')).toBe('bytes 0-2/1024')
expect(await res.text()).toBe('request body: test')
})
})
})
67 changes: 36 additions & 31 deletions src/helper/proxy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,8 @@
* Proxy Helper for Hono.
*/

// Typical header names for requests for proxy use
type ProxyRequestHeaderName = 'X-Forwarded-For' | 'X-Forwarded-Proto' | 'X-Forwarded-Host'

interface ProxyRequestInit extends RequestInit {
/**
* Headers that are overwritten in requests to the origin server.
* Specify undefined to delete the header.
*/
proxySetRequestHeaders?: Partial<Record<ProxyRequestHeaderName, string>> &
Record<string, string | undefined>
/**
* Headers included in the response from the origin server that should be removed in the response to the client.
*/
proxyDeleteResponseHeaderNames?: string[]
raw?: Request
}

interface ProxyFetch {
Expand All @@ -35,45 +23,62 @@ interface ProxyFetch {
* @example
* ```ts
* app.get('/proxy/:path', (c) => {
* return proxyFetch(new Request(`http://${originServer}/${c.req.param('path')}`, c.req.raw), {
* proxySetRequestHeaders: {
* return proxyFetch(`http://${originServer}/${c.req.param('path')}`, {
* headers: {
* ...c.req.header(), // optional, specify only when header forwarding is truly necessary.
* 'X-Forwarded-For': '127.0.0.1',
* 'X-Forwarded-Host': c.req.header('host'),
* Authorization: undefined, // do not propagate request headers contained in c.req.header('Authorization')
* },
* }).then((res) => {
* res.headers.delete('Cookie')
* return res
* })
* })
*
* app.any('/proxy/:path', (c) => {
* return proxyFetch(`http://${originServer}/${c.req.param('path')}`, {
* ...c.req,
* headers: {
* ...c.req.header(),
* 'X-Forwarded-For': '127.0.0.1',
* 'X-Forwarded-Host': c.req.header('host'),
* Authorization: undefined, // do not propagate request headers contained in c.req.raw
* Authorization: undefined, // do not propagate request headers contained in c.req.header('Authorization')
* },
* proxyDeleteResponseHeaderNames: ['Cookie'],
* })
* })
* ```
*/
export const proxyFetch: ProxyFetch = async (input, proxyInit) => {
const {
proxySetRequestHeaders = {},
proxyDeleteResponseHeaderNames = [],
raw,
...requestInit
} = proxyInit ?? {}

const req = new Request(input, requestInit)
req.headers.delete('accept-encoding')

for (const [key, value] of Object.entries(proxySetRequestHeaders)) {
if (value !== undefined) {
req.headers.set(key, value)
} else {
req.headers.delete(key)
}
const requestInitRaw: RequestInit & { duplex?: 'half' } = raw
? {
method: raw.method,
body: raw.body,
headers: raw.headers,
}
: {}
if (requestInitRaw.body) {
requestInitRaw.duplex = 'half'
}

const req = new Request(input, {
...requestInitRaw,
...requestInit,
})
req.headers.delete('accept-encoding')

const res = await fetch(req)
const resHeaders = new Headers(res.headers)
if (resHeaders.has('content-encoding')) {
resHeaders.delete('content-encoding')
// Content-Length is the size of the compressed content, not the size of the original content
resHeaders.delete('content-length')
}
for (const key of proxyDeleteResponseHeaderNames) {
resHeaders.delete(key)
}

return new Response(res.body, {
...res,
Expand Down

0 comments on commit b6eb510

Please sign in to comment.