Skip to content

Commit

Permalink
feat: add proxyRequest util (#226)
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 committed Nov 3, 2022
1 parent e6a9f94 commit 501f0c6
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 8 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ H3 has concept of compasable utilities that accept `event` (from `eventHandler((
- `assertMethod(event, expected, allowHead?)`
- `createError({ statusCode, statusMessage, data? })`
- `sendProxy(event, { target, headers?, fetchOptions?, fetch?, sendStream? })`
- `proxyRequest(event, { target, headers?, fetchOptions?, fetch?, sendStream? })`
👉 You can learn more about usage in [JSDocs Documentation](https://www.jsdocs.io/package/h3#package-functions).
Expand Down
51 changes: 49 additions & 2 deletions src/utils/proxy.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,61 @@
import type { H3Event } from '../event'
import type { RequestHeaders } from '../types'
import { getMethod, getRequestHeaders } from './request'
import { readRawBody } from './body'

export interface SendProxyOptions {
export interface ProxyOptions {
headers?: RequestHeaders | HeadersInit
fetchOptions?: RequestInit
fetch?: typeof fetch
sendStream?: boolean
}

export async function sendProxy (event: H3Event, target: string, opts: SendProxyOptions = {}) {
const PayloadMethods = ['PATCH', 'POST', 'PUT', 'DELETE']
const ignoredHeaders = [
'transfer-encoding',
'connection',
'keep-alive',
'upgrade',
'expect'
]

export async function proxyRequest (event: H3Event, target: string, opts: ProxyOptions = {}) {
// Method
const method = getMethod(event)

// Body
let body
if (PayloadMethods.includes(method)) {
body = await readRawBody(event).catch(() => undefined)
}

// Headers
const headers = Object.create(null)
const reqHeaders = getRequestHeaders(event)
for (const name in reqHeaders) {
if (!ignoredHeaders.includes(name)) {
headers[name] = reqHeaders[name]
}
}
if (opts.fetchOptions?.headers) {
Object.assign(headers, opts.fetchOptions!.headers)
}
if (opts.headers) {
Object.assign(headers, opts.headers)
}

return sendProxy(event, target, {
...opts,
fetchOptions: {
headers,
method,
body,
...opts.fetchOptions
}
})
}

export async function sendProxy (event: H3Event, target: string, opts: ProxyOptions = {}) {
const _fetch = opts.fetch || globalThis.fetch
if (!_fetch) {
throw new Error('fetch is not available. Try importing `node-fetch-native/polyfill` for Node.js.')
Expand Down
49 changes: 43 additions & 6 deletions test/proxy.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
import { Server } from 'node:http'
import supertest, { SuperTest, Test } from 'supertest'
import { describe, it, expect, beforeEach } from 'vitest'
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { fetch } from 'node-fetch-native'
import { createApp, toNodeListener, App, eventHandler } from '../src'
import { sendProxy } from '../src/utils/proxy'
import { createApp, toNodeListener, App, eventHandler, getHeaders, getMethod, readRawBody } from '../src'
import { sendProxy, proxyRequest } from '../src/utils/proxy'

describe('', () => {
let app: App
let request: SuperTest<Test>

beforeEach(() => {
let server: Server
let url: string

beforeEach(async () => {
app = createApp({ debug: false })
request = supertest(toNodeListener(app))
server = new Server(toNodeListener(app))
await new Promise((resolve) => { server.listen(0, () => resolve(null)) })
url = 'http://localhost:' + (server.address() as any).port
})

afterEach(async () => {
await new Promise((resolve) => { server.close(() => resolve(null)) })
})

describe('sendProxy', () => {
Expand All @@ -19,10 +30,36 @@ describe('', () => {
return sendProxy(event, 'https://example.com', { fetch })
}))

const result = await request
.get('/')
const result = await request.get('/')

expect(result.text).toContain('a href="https://www.iana.org/domains/example">More information...</a>')
})
})

describe('proxyRequest', () => {
it('can proxy request', async () => {
app.use('/debug', eventHandler(async (event) => {
const headers = getHeaders(event)
delete headers.host
let body
try { body = await readRawBody(event) } catch {}
return {
method: getMethod(event),
headers,
body
}
}))

app.use('/', eventHandler((event) => {
return proxyRequest(event, url + '/debug', { fetch })
}))

const result = await fetch(url + '/', {
method: 'POST',
body: 'hello'
}).then(r => r.text())

expect(result).toMatchInlineSnapshot('"{\\"method\\":\\"POST\\",\\"headers\\":{\\"accept\\":\\"*/*\\",\\"accept-encoding\\":\\"gzip, deflate, br\\",\\"connection\\":\\"close\\",\\"content-length\\":\\"5\\",\\"content-type\\":\\"text/plain;charset=UTF-8\\",\\"user-agent\\":\\"node-fetch\\"},\\"body\\":\\"hello\\"}"')
})
})
})

0 comments on commit 501f0c6

Please sign in to comment.