Skip to content

Conversation

@pi0
Copy link
Member

@pi0 pi0 commented Nov 4, 2025

Currently nitro patches global fetch with a wrapper that check if request is an string starting with /, dispatches it as an internal fetch. In nitro v2, this was being done using global $fetch instead.

Additionally vite patches global fetch (again) to allow fetching other environments.

This can cause all sorts of implicit behavior with ordering also having this, adds to core bundle size (~1kB + 1kB more for vite).


This PR introduces two new exports: serverFetch and fetch.

The difference is that, serverFetch always dispatches requests to internal routing while fetch does the hybrid method and unless request starts with /, fallbacks to native fetch.

Utils are exported from "nitro" and "nitro/runtime".

Exports from /runtime are bound to nitro instance and only usable in runtime. While importing serverFetch outside of nitro runtime, allows fetchin server routes in Nitro modules and Vite plugins (currently limited to dev mode and one Nitro instance per process)


This PR also updates vite mechanism to avoid wrapper and exporting new fetchViteEnv util from nitro/runtime/vite subpath.

@vercel
Copy link

vercel bot commented Nov 4, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
nitro.build Ready Ready Preview Comment Nov 4, 2025 8:03pm

@pi0 pi0 marked this pull request as ready for review November 4, 2025 17:38
@pkg-pr-new
Copy link

pkg-pr-new bot commented Nov 4, 2025

Open in StackBlitz

npm i https://pkg.pr.new/nitrojs/nitro@3731

commit: e71fe0f

@pi0 pi0 merged commit 029c9b4 into main Nov 4, 2025
7 checks passed
@pi0 pi0 deleted the feat/fetch-exports branch November 4, 2025 20:06
@MickL
Copy link
Contributor

MickL commented Dec 5, 2025

Love it!

A few suggestions, not sure if implemented:

  • event.context should have some flag that is set to true if current request is an internal fetch (e.g. to skip middlewares)
  • fetch() and serverFetch() could have a flag to skip middlewares, in this case event.context should be copied from original request (possible default is true). E.g. a middleware sets event.context.locale for original requests -> middleware does not need to run again and context should be taken from old request
  • Rename fetch to $fetch for better DX? Because when typing fetch() the IDE will not suggest to import it form 'nitro'
  • fetch from nitro has no generics for typing the response const response = await fetch<CustomersResponse>('/customers')

@pi0
Copy link
Member Author

pi0 commented Dec 8, 2025

@MickL skipping middleware (by default) might not be safe. A guard middleware, for example, can be bypassed from an SSR => API request this way. Also, sometimes middleware are used as a conditional routing handler (they intercept the final response).

You might use a specific internal request flag to indicate it or use req.context and use a custom fetch wrapper to do this. Default fetch behavior is as close as possible to a external/normal fetch.

You can also easily use createFetch({ fetch: serverFetch }) to make $fetch for DX.

We should improve fetch types though in later steps.

@MickL
Copy link
Contributor

MickL commented Dec 8, 2025

Thanks for the reply. I would still suggest:

  • Nitro should add a flag if a request is an internal request, e.g. event.context.serverFetch = true so middlewares can be skipped if needed
  • Nitro's fetch shouldnt be named fetch because IDEs will never import Nitro's fetch
  • fetch needs to have response typing. I would suggest to still go with ofetch as before or at least add a generic to set response type.

Why not:

  • Provide nitro/fetch -> ofetch with internal requests if starts with /
  • If one doesnt want ofetch -> dont import nitro/fetch and just use fetch (or other libraries)

@pi0
Copy link
Member Author

pi0 commented Dec 8, 2025

Nitro should add a flag if a request is an internal request,

Internal and external fetch behavior are identical, as the Request web API is abstracted. Feel free to open an issue in H3 though we might do it for app.fetch.

Nitro's fetch shouldnt be named fetch because IDEs will never import Nitro's fetch

That's why we have serverFetch export as well which should auto-complete.

But i think IDE issue is not because of import name. It happens for other subpath imports as well.

fetch needs to have response typing

yes we will add it for paths in the future with an opt-in (typegen) flag.

I would suggest to still go with ofetch as before or at least add a generic to set response type.

ofetch adds runtime DX features mainly. Generic does not needs it. For typing fetch/serverFetch i'm waiting on tooling to be ready (v2 implementation was messy TBH)

@MickL
Copy link
Contributor

MickL commented Dec 8, 2025

Internal and external fetch behavior are identical

I think thats a big issue. I already had this problem with Nitro v1 and it was hard to find a difference in the two event objects. The main reason is that one needs to know if a request is an internal request so certain mechanics can be skipped, e.g. i18n locale is already set, user is already authorized and already authenticated. If all this runs again, there is not much benefit of a server fetch. Instead, original event.context could be added to the new event, maybe as a new variable event.originalContext. ALSO one needs to re-add all headers and cookies that were in the original request to the server fetch because otherwise things like locale detection or authorization wouldnt work, this is also very very unhandy DX and will lead to lots of frustration.

ofetch adds runtime DX features mainly

Yes thats great! :) Seems like you want to get rid of all features but some DX like ofetch offers always has been nice and would still love to see that without the need to build my own fetch wrapper.

Thats why I had the idea, ship (patched) ofetch as an optional import. And if one doesnt want to use it, he can just not import and use native fetch.

@pi0
Copy link
Member Author

pi0 commented Dec 8, 2025

If all this runs again, there is not much benefit of a server fetch

Main benefit of server fetch is that there is no network/TCP round-trip, reducing >ms latency for sub-requests.

I think it is always a good idea to implement an HTTP caching mechanism for session, i18n, etc that works universally best (both for internal and external requests). Of course you can add an internal flag to skip layers but i'm worried it can add to attack surface and reduce deployment flexibility (splitting some parts like SSR and APIs will be harder)

@MickL
Copy link
Contributor

MickL commented Dec 8, 2025

Another important usecase for me is to call another function (handler) e.g. when a webhook from Stripe comes in saying "product sold". Now I want to call my product-update-handler PATCH /product/:id to set status to sold. I cant duplicate the code because there are way more things running in this handler like updating Algolia. The problem is: Any request needs to be authenticated but the initial request came from a webhook, so there is no user authentication, I need to skip authentication middleware for calling the patch function.

Another different solution would be if handlers can be called directly as a function call without fetch.

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.

3 participants