Skip to content
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

Middleware #1521

Merged
merged 8 commits into from
Feb 15, 2024
Merged

Middleware #1521

merged 8 commits into from
Feb 15, 2024

Conversation

drwpow
Copy link
Owner

@drwpow drwpow commented Jan 28, 2024

Changes

Addresses #1122 by adding Middleware support. This is an RFC so feedback is welcome! This will be used for testing & feedback before stable release.

You can download the beta version at the @next tag:

npm i openapi-fetch@next

Dev Notes

Please read the docs first to learn about the API. The following are more “meta” notes helpful for early reviewers:

  • The API was suggested by @denisw 🎉. I found it to be easy-to-use and clear in typing.
    • I pulled inspiration from tRPC links, Hono middleware, Next.js middleware, and Axios interceptors, but ultimately landed on a semi-unique API that’s a good blend of idiomatic and performant/lightweight.
    • A lot of middleware complexity stems from server streaming, which this library isn’t concerned with (as the 99% usecase is JSON, which is not streamable).
    • A simple middleware approach is not only easy-to-use; it’s also performant and lightweight (the final implementation only added a couple dozen lines to the codebase, and no performance impact).
  • There’s still no next() callback.
    • In server middleware, this is necessary when streaming, or wanting to return a response to the user earlier but have some server work to do (logging, etc.).
    • But in fetch() land, and in my early testing, I found it to be a little overcomplicated since JSON responses (which this relies on) doesn’t utilize streaming. And to start I wanted to prefer a simpler middleware structure over a complex one (i.e. not encourage over-engineering clientside server setups)
  • The .use(middleware) seemed to be a ubiquitous way to register middlewares
    • Originally, there was a createClient({ middlewares: […] }) array, which doesn’t have any advantages, and only limits how/when middlewares are loaded.
    • The less-commonly-used .eject() was inspired by Axios interceptors. It may or may not get use, but was essentially free. So why not?

How to Review

  • 📚 Read the docs
  • Test out the latest version at openapi-fetch@next.

Checklist

  • Unit tests updated
  • docs/ updated (if necessary)
  • pnpm run update:examples run (only applicable for openapi-typescript)

Copy link

changeset-bot bot commented Jan 28, 2024

🦋 Changeset detected

Latest commit: fc3a468

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
openapi-fetch Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link

cloudflare-pages bot commented Jan 28, 2024

Deploying with  Cloudflare Pages  Cloudflare Pages

Latest commit: fc3a468
Status: ✅  Deploy successful!
Preview URL: https://7779179f.openapi-ts.pages.dev
Branch Preview URL: https://middleware.openapi-ts.pages.dev

View logs

@drwpow drwpow force-pushed the middleware branch 2 times, most recently from 77f2787 to f6bcb23 Compare January 28, 2024 22:23

const {
data, // only present if 2XX response
error, // only present if 4XX or 5XX response
} = await GET("/blogposts/{post_id}", {
} = await client.GET("/blogposts/{post_id}", {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extremely in-the-weeds: when writing the docs for middleware, I found const { use } = createClient() to be an awkward syntax (it it just me or does a floating use() just seem weird?). Renaming it (const { use: useMiddleware }) also felt awkward. client.use(…) felt like the most natural syntax, and matched other libraries.

As a result, it felt a little weird in other places to both keep client around and destructure it, so I opted to simplify and just keep client around in all places. Really, there’s no recommended syntax, and people can do what they want. I’m just trying to write the docs in the most “copy-pasteable” way 🤷

import type { paths } from "./v1";

const throwOnError: Middleware = {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

People have asked for this: a more automatic way to interface with React Query. Hopefully this is sufficient!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel this may be generally useful enough to deserve a call out in the middleware docs (even if just cross linking to the example). It may get missed otherwise

@@ -4,130 +4,28 @@ title: openapi-fetch Examples

# Examples

## Authentication
Copy link
Owner Author

@drwpow drwpow Jan 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With middleware, these awkward, one-off workarounds can go away. Now it can be handled more-automatically (also this entire section was rewritten, and moved to the middleware page).

@@ -81,26 +116,17 @@ export default function createClient(clientOptions) {
if (response.ok) {
// if "stream", skip parsing entirely
if (parseAs === "stream") {
// fix for bun: bun consumes response.body, therefore clone before accessing
// TODO: test this?
return { data: response.clone().body, response };
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed a lot of the .clone()-ing that was happening internally. I profiled this, and this can be a pretty big bottleneck, in addition to a waste of memory for large responses.

Instead, I’d rather users learn about safe vs unsafe body access rather than shield them from this. openapi-fetch will consume the body by calling .json() automatically (because 99% of the time that’s what users want). And if users need to read the raw body, then they’re doing something advanced and should know to .clone().

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This related Bun issue here (#1486) I think is a mixture of a potential bug in Bun (which may be patched by now) along with user error. In either case, will investigate separately rather than cause potential performance bottlenecks for everyone.

packages/openapi-fetch/test/index.test.ts Dismissed Show resolved Hide resolved
packages/openapi-fetch/test/v7-beta.test.ts Dismissed Show dismissed Hide dismissed
docs/openapi-fetch/api.md Outdated Show resolved Hide resolved
docs/openapi-fetch/api.md Outdated Show resolved Hide resolved
@Noahkoole
Copy link

Found another issue with type safety. #1525

@drwpow drwpow force-pushed the middleware branch 3 times, most recently from f8e3b5e to db0ada7 Compare February 15, 2024 17:28

| Library | Size (min) | “GET” request |
| :------------------------- | ---------: | :------------------------- |
| openapi-fetch | `4 kB` | `278k` ops/s (fastest) |
| openapi-fetch | `5 kB` | `278k` ops/s (fastest) |
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤷 sometimes the bytes do things


## Mocking Requests

To test requests, the `fetch` option can be supplied with any spy function like `vi.fn()` (Vitest) or `jest.fn()` (Jest).
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 I’ll probably rewrite this to recommend MSW as I’ve had some good success with it, but will probably wait until I refactor this codebase to use it for testing. Treat this as more of a placeholder.

});

// expect post_id to be encoded properly
expect(fetchMocker.mock.calls[0][0]).toBe(`/blogposts/${post_id}`);
expect(fetchMocker.mock.calls[0][0].url).toBe(
`/blogposts/post?id%20=%20🥴`,
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side-effect of using new Request()—we’re not encoding anything manually. Seems OK? Still tests UTF-8 sticks

@drwpow drwpow merged commit c61c472 into main Feb 15, 2024
8 checks passed
@drwpow drwpow deleted the middleware branch February 15, 2024 17:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants