Skip to content

fetch middware rework #1556

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
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
24 changes: 20 additions & 4 deletions docs/openapi-fetch/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,8 @@ onRequest(req, options) {

| Name | Type | Description |
| :-------- | :-----------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `req` | `MiddlewareRequest` | A standard [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) with `schemaPath` (OpenAPI pathname) and `params` ([params](/openapi-fetch/api#fetch-options) object) |
| `options` | `MergedOptions` | Combination of [createClient](/openapi-fetch/api#create-client) options + [fetch overrides](/openapi-fetch/api#fetch-options) |
| `req` | `Request` | A standard [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) |
| `options` | `MergedOptions` | Combination of [createClient](/openapi-fetch/api#create-client) options + [fetch overrides](/openapi-fetch/api#fetch-options) with `schemaPath` (OpenAPI pathname) and `params` ([params](/openapi-fetch/api#fetch-options) object) |

And it expects either:

Expand All @@ -214,8 +214,8 @@ onResponse(res, options) {
`onResponse()` also takes 2 params:
| Name | Type | Description |
| :-------- | :-----------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `req` | `MiddlewareRequest` | A standard [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response). |
| `options` | `MergedOptions` | Combination of [createClient](/openapi-fetch/api#create-client) options + [fetch overrides](/openapi-fetch/api#fetch-options) |
| `req` | `Request` | A standard [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response). |
| `options` | `MergedOptions` | Combination of [createClient](/openapi-fetch/api#create-client) options + [fetch overrides](/openapi-fetch/api#fetch-options) with `schemaPath` (OpenAPI pathname) and `params` ([params](/openapi-fetch/api#fetch-options) object) ||

And it expects either:

Expand All @@ -237,6 +237,22 @@ onRequest(req) {

This will leave the request/response unmodified, and pass things off to the next middleware handler (if any). There’s no internal callback or observer library needed.

### Resend

If you want to resend some request in middleware, just use `send` with the `request` and `options` param.

```ts
async onResponse(response, options) {
if(response.status === 401) {
// do some other request...like refresh the token by refreshToken, then update it
refreshToken();
// then resend it.
return await client.send(new Request(options.requestUrl,options.requestOptions), options);
}
return response;
},
```

### Ejecting middleware

To remove middleware, call `client.eject(middleware)`:
Expand Down
12 changes: 7 additions & 5 deletions packages/openapi-fetch/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,6 @@ export type MergedOptions<T = unknown> = {
querySerializer: QuerySerializer<T>;
bodySerializer: BodySerializer<T>;
fetch: typeof globalThis.fetch;
};

export interface MiddlewareRequest extends Request {
/** The original OpenAPI schema path (including curly braces) */
schemaPath: string;
/** OpenAPI parameters as provided from openapi-fetch */
Expand All @@ -158,10 +155,13 @@ export interface MiddlewareRequest extends Request {
path?: Record<string, unknown>;
cookie?: Record<string, unknown>;
};
}
/** use to build a new request object. */
requestUrl: string;
requestOptions: RequestInit;
};

export function onRequest(
req: MiddlewareRequest,
req: Request,
options: MergedOptions,
): Request | undefined | Promise<Request | undefined>;
export function onResponse(
Expand Down Expand Up @@ -201,6 +201,8 @@ export default function createClient<Paths extends {}>(
PATCH: ClientMethod<Paths, "patch">;
/** Call a TRACE endpoint */
TRACE: ClientMethod<Paths, "trace">;
/** the core send just with middleware */
send(request:Request, options?: MergedOptions): Promise<Response>;
/** Register middleware */
use(...middleware: Middleware[]): void;
/** Unregister middleware */
Expand Down
79 changes: 47 additions & 32 deletions packages/openapi-fetch/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,22 +69,59 @@ export default function createClient(clientOptions) {
if (requestInit.body instanceof FormData) {
requestInit.headers.delete("Content-Type");
}
let request = new Request(
createFinalURL(url, { baseUrl, params, querySerializer }),
requestInit,
);

const finalUrl = createFinalURL(url, { baseUrl, params, querySerializer });
const finalOptions = requestInit;
const request = new Request(finalUrl, finalOptions);
// middleware (request)
const mergedOptions = {
baseUrl,
fetch,
parseAs,
querySerializer,
bodySerializer,
schemaPath: url,
params,
requestUrl: finalUrl,
requestOptions: finalOptions
};
const response = await coreSend(request, mergedOptions)

// handle empty content
// note: we return `{}` because we want user truthy checks for `.data` or `.error` to succeed
if (
response.status === 204 ||
response.headers.get("Content-Length") === "0"
) {
return response.ok ? { data: {}, response } : { error: {}, response };
}

// parse response (falling back to .text() when necessary)
if (response.ok) {
// if "stream", skip parsing entirely
if (parseAs === "stream") {
return { data: response.body, response };
}
return { data: await response[parseAs](), response };
}

// handle errors
let error = await response.text();
try {
error = JSON.parse(error); // attempt to parse as JSON
} catch {
// noop
}
return { error, response };
}
/**
* send raw request with middleware
*/
async function coreSend(request, mergedOptions){
// set default value for sometimes user use send directly but not provide options
const {fetch = globalThis.fetch} = mergedOptions || {};
for (const m of middlewares) {
if (m && typeof m === "object" && typeof m.onRequest === "function") {
request.schemaPath = url; // (re)attach original URL
request.params = params; // (re)attach params
const result = await m.onRequest(request, mergedOptions);
if (result) {
if (!(result instanceof Request)) {
Expand Down Expand Up @@ -117,32 +154,7 @@ export default function createClient(clientOptions) {
}
}

// handle empty content
// note: we return `{}` because we want user truthy checks for `.data` or `.error` to succeed
if (
response.status === 204 ||
response.headers.get("Content-Length") === "0"
) {
return response.ok ? { data: {}, response } : { error: {}, response };
}

// parse response (falling back to .text() when necessary)
if (response.ok) {
// if "stream", skip parsing entirely
if (parseAs === "stream") {
return { data: response.body, response };
}
return { data: await response[parseAs](), response };
}

// handle errors
let error = await response.text();
try {
error = JSON.parse(error); // attempt to parse as JSON
} catch {
// noop
}
return { error, response };
return response;
}

return {
Expand Down Expand Up @@ -178,6 +190,9 @@ export default function createClient(clientOptions) {
async TRACE(url, init) {
return coreFetch(url, { ...init, method: "TRACE" });
},
async send(request, options) {
return await coreSend(request, options);
},
/** Register middleware */
use(...middleware) {
for (const m of middleware) {
Expand Down
104 changes: 99 additions & 5 deletions packages/openapi-fetch/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
// @ts-expect-error
import createFetchMock from "vitest-fetch-mock";
import createClient, {
type MergedOptions,
type Middleware,
type MiddlewareRequest,
type QuerySerializerOptions,
} from "../src/index.js";
import type { paths } from "./fixtures/api.js";
Expand Down Expand Up @@ -855,15 +855,15 @@ describe("client", () => {
};

let receivedPath = "";
let receivedParams: MiddlewareRequest["params"] = {};
let receivedParams: MergedOptions["params"] = {};

const client = createClient<paths>({
baseUrl: "https://api.foo.bar/v1/",
});
client.use({
onRequest(req) {
receivedPath = req!.schemaPath;
receivedParams = req!.params;
onRequest(req, options) {
receivedPath = options!.schemaPath;
receivedParams = options!.params;
return undefined;
},
});
Expand All @@ -889,6 +889,100 @@ describe("client", () => {
expect(data).toEqual({ success: true });
});

it("can invoke send directly", async () => {
mockFetchOnce({ status: 200, body: JSON.stringify({ success: true }) });
const client = createClient<paths>({
baseUrl: "https://api.foo.bar/v1/",
});
const response = await client.send(new Request('/blogposts', { method: 'GET' }));
const data = await response.json();
expect(data).toEqual({ success: true });
});

it("can get the final url in merged options", async () => {
mockFetchOnce({ status: 200, body: JSON.stringify({ success: true }) });
const client = createClient<paths>({
baseUrl: "https://api.foo.bar/v1/",
});
let receivedUrl = '';
client.use({
onRequest(_, options) {
receivedUrl = options.requestUrl;
return undefined;
},
})
await client.GET("/blogposts/{id}", { params: { path: { id: "123" } } });
expect(receivedUrl).toBe("https://api.foo.bar/v1/blogposts/123");
});
it("can get the final request options in merged options", async () => {
mockFetchOnce({ status: 200, body: JSON.stringify({ success: true }) });
const client = createClient<paths>({
baseUrl: "https://api.foo.bar/v1/",
});
let receivedOptions: RequestInit = {};
client.use({
onRequest(_, options) {
receivedOptions = options.requestOptions;
return undefined;
},
})
await client.POST("/blogposts/{id}", { params: { path: { id: "123" } }, body: { title: "New Post"}});
expect(receivedOptions.method).toBe("POST");
expect(receivedOptions.body).toBe(JSON.stringify({ title: "New Post"}));
});
it("can invoke send with middleware", async () => {
mockFetchOnce({ status: 200, body: JSON.stringify({ success: true }) });
const client = createClient<paths>({
baseUrl: "https://api.foo.bar/v1/",
});
const targetUrl = 'https://api.foo.bar/v2/';
client.use({
onRequest() {
return new Request(targetUrl);
},
});
let receivedUrl = '';
client.use({
onRequest(request) {
receivedUrl = request.url;
return undefined;
},
})
await client.send(new Request('/blogposts', { method: 'GET' }));
expect(receivedUrl).toEqual(targetUrl);
});
it("can resend request in middleware", async () => {
fetchMocker.mockResponses(
[JSON.stringify({ success: false }), {status: 401}],
[JSON.stringify({ success: true }), {status: 200}],
)
const client = createClient<paths>({
baseUrl: "https://api.foo.bar/v1/",
});
let firstTimeData = {};
const requestUrls:string[] =[];
client.use({
async onRequest(request){
requestUrls.push(request.url);
return undefined;
},
async onResponse(response, options) {
if(response.status === 401) {
// do some other request...
// then resend it.
firstTimeData = await response.clone().json();
return await client.send(new Request(options.requestUrl,options.requestOptions), options);
}
return response;
},
});
const { data } = await client.GET("/blogposts");
// reqeust 2 times with same request.
expect(requestUrls).toMatchObject(["https://api.foo.bar/v1/blogposts", "https://api.foo.bar/v1/blogposts"]);
expect(firstTimeData).toEqual({ success: false });
expect(data).toEqual({ success: true });
});

it("can be ejected", async () => {
mockFetchOnce({ status: 200, body: "{}" });

Expand Down
10 changes: 5 additions & 5 deletions packages/openapi-fetch/test/v7-beta.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
// @ts-expect-error
import createFetchMock from "vitest-fetch-mock";
import createClient, {
MergedOptions,
type Middleware,
type MiddlewareRequest,
type QuerySerializerOptions,
} from "../src/index.js";
import type { paths } from "./fixtures/v7-beta.js";
Expand Down Expand Up @@ -865,15 +865,15 @@ describe("client", () => {
};

let receivedPath = "";
let receivedParams: MiddlewareRequest["params"] = {};
let receivedParams: MergedOptions["params"] = {};

const client = createClient<paths>({
baseUrl: "https://api.foo.bar/v1/",
});
client.use({
onRequest(req) {
receivedPath = req!.schemaPath;
receivedParams = req!.params;
onRequest(req,options) {
receivedPath = options!.schemaPath;
receivedParams = options!.params;
return undefined;
},
});
Expand Down