nitro icon indicating copy to clipboard operation
nitro copied to clipboard

feat: server fetch utils

Open pi0 opened this issue 1 month ago • 1 comments

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).


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.

pi0 avatar Nov 04 '25 16:11 pi0

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

vercel[bot] avatar Nov 04 '25 16:11 vercel[bot]

Open in StackBlitz

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

commit: e71fe0f

pkg-pr-new[bot] avatar Nov 04 '25 20:11 pkg-pr-new[bot]

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')

MickL avatar Dec 05 '25 14:12 MickL

@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.

pi0 avatar Dec 08 '25 11:12 pi0

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)

MickL avatar Dec 08 '25 12:12 MickL

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)

pi0 avatar Dec 08 '25 12:12 pi0

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.

MickL avatar Dec 08 '25 12:12 MickL

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)

pi0 avatar Dec 08 '25 12:12 pi0

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.

MickL avatar Dec 08 '25 13:12 MickL