From fb07fb4012972cc48785678fd52a8810b8cce169 Mon Sep 17 00:00:00 2001 From: Trung Dang Date: Fri, 3 May 2024 07:02:54 +0700 Subject: [PATCH] feat(hc): add `init` option (#2592) --- deno_dist/client/client.ts | 1 + deno_dist/client/types.ts | 15 +++++-- src/client/client.test.ts | 91 ++++++++++++++++++++++++++++++++++++++ src/client/client.ts | 1 + src/client/types.ts | 15 +++++-- 5 files changed, 115 insertions(+), 8 deletions(-) diff --git a/deno_dist/client/client.ts b/deno_dist/client/client.ts index cd3b22ce..632b2c5d 100644 --- a/deno_dist/client/client.ts +++ b/deno_dist/client/client.ts @@ -123,6 +123,7 @@ class ClientRequestImpl { body: setBody ? this.rBody : undefined, method: methodUpperCase, headers: headers, + ...opt?.init, }) } } diff --git a/deno_dist/client/types.ts b/deno_dist/client/types.ts index 781b268f..0b3c776d 100644 --- a/deno_dist/client/types.ts +++ b/deno_dist/client/types.ts @@ -5,17 +5,24 @@ import type { HasRequiredKeys } from '../utils/types.ts' type HonoRequest = (typeof Hono.prototype)['request'] -export type ClientRequestOptions = keyof T extends never +export type ClientRequestOptions = { + fetch?: typeof fetch | HonoRequest + /** + * Standard `RequestInit`, caution that this take highest priority + * and could be used to overwrite things that Hono sets for you, like `body | method | headers`. + * + * If you want to add some headers, use in `headers` instead of `init` + */ + init?: RequestInit +} & (keyof T extends never ? { headers?: | Record | (() => Record | Promise>) - fetch?: typeof fetch | HonoRequest } : { headers: T | (() => T | Promise) - fetch?: typeof fetch | HonoRequest - } + }) export type ClientRequest = { [M in keyof S]: S[M] extends Endpoint & { input: infer R } diff --git a/src/client/client.test.ts b/src/client/client.test.ts index d2aadac9..bb00264d 100644 --- a/src/client/client.test.ts +++ b/src/client/client.test.ts @@ -828,6 +828,97 @@ describe('Dynamic headers', () => { }) }) +describe('RequestInit work as expected', () => { + const app = new Hono() + + const route = app + .get('/credentials', (c) => { + return c.text('' as RequestCredentials) + }) + .get('/headers', (c) => { + return c.json({} as Record) + }) + .post('/headers', (c) => c.text('Not found', 404)) + + type AppType = typeof route + + const server = setupServer( + rest.get('http://localhost/credentials', async (req, res, ctx) => { + return res(ctx.status(200), ctx.text(req.credentials)) + }), + rest.get('http://localhost/headers', async (req, res, ctx) => { + const allHeaders: Record = {} + for (const [k, v] of req.headers.entries()) { + allHeaders[k] = v + } + + return res(ctx.status(200), ctx.json(allHeaders)) + }), + rest.post('http://localhost/headers', async (req, res, ctx) => { + return res(ctx.status(400), ctx.text('Should not be here')) + }) + ) + + beforeAll(() => server.listen()) + afterEach(() => server.resetHandlers()) + afterAll(() => server.close()) + + const client = hc('http://localhost', { + headers: { 'x-hono': 'fire' }, + init: { + credentials: 'include', + }, + }) + + it('Should overwrite method and fail', async () => { + const res = await client.headers.$get(undefined, { init: { method: 'POST' } }) + + expect(res.ok).toBe(false) + }) + + it('Should clear headers', async () => { + const res = await client.headers.$get(undefined, { init: { headers: undefined } }) + + expect(res.ok).toBe(true) + const data = await res.json() + expect(data).toEqual({}) + }) + + it('Should overwrite headers', async () => { + const res = await client.headers.$get(undefined, { + init: { headers: new Headers({ 'x-hono': 'awesome' }) }, + }) + + expect(res.ok).toBe(true) + const data = await res.json() + expect(data).toEqual({ 'x-hono': 'awesome' }) + }) + + it('credentials is include', async () => { + const res = await client.credentials.$get() + + expect(res.ok).toBe(true) + const data = await res.text() + expect(data).toEqual('include') + }) + + it('deepMerge should works and not unset credentials', async () => { + const res = await client.credentials.$get(undefined, { init: { headers: { hi: 'hello' } } }) + + expect(res.ok).toBe(true) + const data = await res.text() + expect(data).toEqual('include') + }) + + it('Should unset credentials', async () => { + const res = await client.credentials.$get(undefined, { init: { credentials: undefined } }) + + expect(res.ok).toBe(true) + const data = await res.text() + expect(data).toEqual('same-origin') + }) +}) + describe('WebSocket URL Protocol Translation', () => { const app = new Hono() const route = app.get( diff --git a/src/client/client.ts b/src/client/client.ts index 366ba020..c6e7f5c0 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -123,6 +123,7 @@ class ClientRequestImpl { body: setBody ? this.rBody : undefined, method: methodUpperCase, headers: headers, + ...opt?.init, }) } } diff --git a/src/client/types.ts b/src/client/types.ts index 48415e87..0b76804b 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -5,17 +5,24 @@ import type { HasRequiredKeys } from '../utils/types' type HonoRequest = (typeof Hono.prototype)['request'] -export type ClientRequestOptions = keyof T extends never +export type ClientRequestOptions = { + fetch?: typeof fetch | HonoRequest + /** + * Standard `RequestInit`, caution that this take highest priority + * and could be used to overwrite things that Hono sets for you, like `body | method | headers`. + * + * If you want to add some headers, use in `headers` instead of `init` + */ + init?: RequestInit +} & (keyof T extends never ? { headers?: | Record | (() => Record | Promise>) - fetch?: typeof fetch | HonoRequest } : { headers: T | (() => T | Promise) - fetch?: typeof fetch | HonoRequest - } + }) export type ClientRequest = { [M in keyof S]: S[M] extends Endpoint & { input: infer R }