From 501f0c6e623ea827d47691046f3c7319f5ac4651 Mon Sep 17 00:00:00 2001 From: pooya parsa Date: Thu, 3 Nov 2022 20:49:49 +0100 Subject: [PATCH] feat: add `proxyRequest` util (#226) --- README.md | 1 + src/utils/proxy.ts | 51 ++++++++++++++++++++++++++++++++++++++++++++-- test/proxy.test.ts | 49 ++++++++++++++++++++++++++++++++++++++------ 3 files changed, 93 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index ed105ab4..4c2b9ce4 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/src/utils/proxy.ts b/src/utils/proxy.ts index b57872c0..a0c6b4d0 100644 --- a/src/utils/proxy.ts +++ b/src/utils/proxy.ts @@ -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.') diff --git a/test/proxy.test.ts b/test/proxy.test.ts index 7517a823..4f061b89 100644 --- a/test/proxy.test.ts +++ b/test/proxy.test.ts @@ -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 - 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', () => { @@ -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...') }) }) + + 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\\"}"') + }) + }) })