Repository navigation
Replies: 23 comments 10 replies
|
Another important use case for middleware is database connection pooling. We need to be able to ensure the database connections are released back to the pool at end end of the request even if an error is thrown in a loader/action. So for now we need to use this function useDB(fn) {
const db = POOL.get();
try {
await fn(db);
} finally {
db.release();
}
}This is OKish but it makes loaders and actions needlessly nested: export const loader: LoaderFunction = async () => {
await useDB(async (db) => {
await db.query(...)
});
};It would be awesome to have a central place to extend the loader/action context so we could implement something like this: export const loader: LoaderFunction = async ({ db }) => {
await db.query(...)
}; |
|
@esamattis good use case. It seems like This would be at least feasible to improvise in userland in the meantime, with a custom loader/action higher-order wrapper - function dbLoader(loaderImpl: (event: Parameters<LoaderFunction>[0], db: DbPool) => ReturnType<LoaderFunction>): LoaderFunction {
const loader: LoaderFunction = (event) => {
return useDB((db) => {
return loaderImpl(event, db);
});
}
return loader;
}
// ...
export const loader = dbLoader(async ({ request }, db) => {
await db.query(...);
}); |
|
I think user land middleware should be now possible with
Is it executed on loaders and actions too? Seems like it is always doing react rending π€
This is actually pretty good solution but I wonder whether does it get properly removed from the browser builds. Thanks for the ideas! I'll be investigating/testing these more on next week! |
Did some testing and it does not get removed from the browser bundles under UPDATE: Sorry, plain function elimination / pruning works. I just had some side effects in the module while testing. But that explains why the higher order function does not work: It is a side-effect. |
|
Hm, that's troubling (probably worth a second issue). I'd hope that we could introduce composition functions over loaders and actions in userland without any problems. |
|
I was looking for a graceful way to handle subdomain multi-tenant logic. Out of the box support would be much appreciated. Same goes for language/region/city detection. I can technically use a loader in root.tsx, but I think this logic is application-wide and doesn't necessarily belong to anything related to react. |
|
There is no need for a middleware concept for multi-tenancy apps (see my comment on #1337). |
|
That will only work if route paths match exactly between subdomain situations and non-subdomain - i.e. But if This ballooning complexity problem is what I was referring to in the original issue text. The only simple solution I can see is to rewrite |
|
@a-type Yes, I agree - in my use case, the routing structure does not change based on different tenants. The ability to perform rewrites in middleware would open more complex use cases and I would also like to see it. If you are on Cloudflare, you could dedicate a worker just for doing the rewrites. So there are workarounds, although I would prefer a native remix solution. |
|
One of the reasons why Remix doesn't support rewriting URLs is that the URL that it's processing must match the URL that is in the address bar. This is because React Router (which Remix is built on) does client-side routing so the server URL must match the client URL. They would have to include URL rewriting in React Router itself. |
|
Maybe I don't understand the depth of the issue, but wouldn't it be possible to have a middleware/proxy before entering React router? All the rewrites would be done in the middleware layer. |
|
The client-side routing definitely complicates things! Seems like the middleware may have to live in RR itself. However, that also means that even solutions like rewriting the incoming HTTP request in your lambda layer won't work, right? It would seem Remix doesn't have any solution for the kind of use case you see in NextJS' Multi-Tenant demo using their middleware concepts. I'm not sure how NextJS middleware interacts with their client-side routing, but they've specifically showcased this kind of thing which at least validates that Vercel sees the need for this use case enough to commit effort to documenting it. |
|
Just to chime in here, this is also something that would be an appropriate entry point for functionality like Sentry to inject itself on the server. I think the pattern of having something like Since folks are contrasting to Next.js, I will say that their approach to middleware is good in some situations, but still limiting in others. For example, if you use Google's Identity-Aware Proxy it passes a signed JWT to every request which contains the authentication information. In an ideal world an app (a Remix app) could parse this and bundle that information up in its hydration phase (middleware?). It could then retain that state in memory to understand if/who the authenticated session is. AFAIK in Next.js the only solution here is to utilize cookies to pass that information. There may be other approaches but it'd certainly be convulated. Similarly in Sentry we'd want to inject something as a middleware that ideally could access things like routing information, HTTP headers, etc. allowing us to enrich data streams (both for error reporting and tracing). That enrichment behavior would be fairly distinct on the server, but would still be desirable on the client. So while it's certainly possible you could come up with an API that lets you write-once-run-everywhere middleware, I personally would be fine with having an implementation for each runtime. |
|
+1 on this. I'm writing an app right now that's using an external API that uses bearer auth and I'd like to add some server middleware to get my auth token from an HTTP-only and add it to the Is my solution just to use an Express server and write middleware there? |
|
I to think it would be pretty powerful if Remix could provide a way without requiring a custom express server (or node at all) Would like to do redirects before Remix and inject my db instance like Or maybe I just haven't properly figured out how to bundle the server.ts with Remix π€π |
|
For anyone interested, I started a patch that I have been using locally to solve for some of the "global, once per request" logic as well as global server-side error handling (eg: pushing to sentry) entry.server.tsx export function handleError(request: Request, error: Error, context: any) {
const logger = container.get<Logger>(Binding.LOGGER);
logger.error(error);
}
export async function buildLoadContext(request: Request): Promise<RequestContext> {
// create container (inversify) on first request, re-use container on subsequent requests
await buildContainer();
// create a unique ORM entity manager for each request
const factory = await container.getAsync(MikroManagerFactory);
return {
log: container.get(Binding.LOGGER),
mikro: factory.create(),
container, // allow routes to pull services from container
};
}In theory, you could use the same load context function to add conditional guards (eg: if path = x or params = y), but a cleaner solution would be needed to define at the path/folder route level. patch: diff --git a/dist/server.js b/dist/server.js
index eb21a54aeafb4265d6fc9b31b576245cc1b44538..ddceaf275fa8c0de5b04aece0bdd43649ce3040d 100644
--- a/dist/server.js
+++ b/dist/server.js
@@ -23,17 +23,25 @@ var responses = require('./responses.js');
var serverHandoff = require('./serverHandoff.js');
const createRequestHandler = (build, mode$1) => {
+ if (build.entry.module.handleError === undefined) {
+ build.entry.module.handleError = (() => {})
+ }
let routes$1 = routes.createRoutes(build.routes);
let serverMode = mode.isServerMode(mode$1) ? mode$1 : mode.ServerMode.Production;
return async function requestHandler(request, loadContext) {
let url = new URL(request.url);
let matches = routeMatching.matchServerRoutes(routes$1, url.pathname);
let response;
+ let lc = loadContext;
+ if(build.entry.module.buildLoadContext){
+ lc = await build.entry.module.buildLoadContext(request);
+ }
if (url.searchParams.has("_data")) {
response = await handleDataRequest({
request,
- loadContext,
+ loadContext: lc,
+ handleError: build.entry.module.handleError,
matches: matches,
handleDataRequest: build.entry.module.handleDataRequest,
serverMode
@@ -41,14 +49,16 @@ const createRequestHandler = (build, mode$1) => {
} else if (matches && !matches[matches.length - 1].route.module.default) {
response = await handleResourceRequest({
request,
- loadContext,
+ loadContext: lc,
+ handleError: build.entry.module.handleError,
matches,
serverMode
});
} else {
response = await handleDocumentRequest({
build,
- loadContext,
+ loadContext: lc,
+ handleError: build.entry.module.handleError,
matches,
request,
routes: routes$1,
@@ -71,6 +81,7 @@ const createRequestHandler = (build, mode$1) => {
async function handleDataRequest({
handleDataRequest,
loadContext,
+ handleError,
matches,
request,
serverMode
@@ -145,9 +156,7 @@ async function handleDataRequest({
return response;
} catch (error) {
- if (serverMode !== mode.ServerMode.Test) {
- console.error(error);
- }
+ handleError(request, error, loadContext);
if (serverMode === mode.ServerMode.Development) {
return errorBoundaryError(error, 500);
@@ -160,6 +169,7 @@ async function handleDataRequest({
async function handleDocumentRequest({
build,
loadContext,
+ handleError,
matches,
request,
routes,
@@ -233,9 +243,7 @@ async function handleDocumentRequest({
appState.trackBoundaries = false;
appState.error = await errors.serializeError(error);
- if (serverMode !== mode.ServerMode.Test) {
- console.error(`There was an error running the action for route ${actionMatch.route.id}`);
- }
+ handleError(request, error, loadContext);
}
}
@@ -315,9 +323,7 @@ async function handleDocumentRequest({
appState.trackBoundaries = false;
appState.error = await errors.serializeError(error);
- if (serverMode !== mode.ServerMode.Test) {
- console.error(`There was an error running the data loader for route ${match.route.id}`);
- }
+ handleError(request, error, loadContext);
break;
} else if (response) {
@@ -408,9 +414,7 @@ async function handleDocumentRequest({
try {
return await handleDocumentRequest(request, responseStatusCode, responseHeaders, entryContext);
} catch (error) {
- if (serverMode !== mode.ServerMode.Test) {
- console.error(error);
- }
+ handleError(request, error, loadContext);
let message = "Unexpected Server Error";
@@ -431,6 +435,7 @@ async function handleDocumentRequest({
async function handleResourceRequest({
loadContext,
+ handleError,
matches,
request,
serverMode
@@ -452,9 +457,7 @@ async function handleResourceRequest({
});
}
} catch (error) {
- if (serverMode !== mode.ServerMode.Test) {
- console.error(error);
- }
+ handleError(request, error, loadContext);
let message = "Unexpected Server Error";
|
|
Unfortunately, the regular Remix App Server doesn't expose Express API directly and I don't want to patch or hack the internals to make it work I'm currently exploring middleware right now through the following templates:
|
|
Middleware for me is the biggest remaining feature Remix needs so I'm excited to see the new API @kentcdodds. I want to humbly express some of my requirements so that they can be considred for the design. Requirements
This would greatly simplify code and improve testing and security in more complex serverless applications. The UX for localization is also critical for complex apps and middleware would solve issues like #3847 #3355. Sentry integration also wouldn't require manually monkey patching the server build object sentry-remix-node.js#L51. DX would be improved and a wide range of functional and security bugs avoided from devs forgetting to add cross-cutting concerns to their loaders/actions like csrf, session commits and db connection releasing. I could imagine middleware looking something like the below but it sounds like the new api may provide something even better. Some middleware could look like the following based on koa 2/.net core middleware. An important feature is waitUntil that blocks the navigation until the promise resolves as discussed here by @sergiodxa. It mkaes sense to me in client middleware but I don't know about on the server. Dependency Injection Support |
|
Hi everyone! Any updates on this feature? I really want to create my new project on top of remix but this feature is a major requirement. |
|
It's been some months since the last update. Are there any updated on this topic? π It would be awesome to just have a middleware or something which can be executed before the loaders to check user auth / permissions. |
|
Is there a way for us to check progress? Is it currently possible to intercept all calls to |
|
Hi any update on plans for middleware? |
|
This discussion is being closed as part of the Remix v3 repository cleanup. Remix v1/v2 was upstreamed into React Router v7, so if this proposal is still relevant, please reopen or continue the discussion in the React Router repository. |

Uh oh!
There was an error while loading. Please reload this page.
What is the new or updated feature that you are suggesting?
Enjoying Remix so far, but there's one key feature I'd like to see for multi-tenant apps: the ability to intercept and modify an incoming request before handing it off to the core Remix runtime which is agnostic of the deployed platform.
The multi-tenant use case is to enable wildcard subdomains which route to a particular nested route structure within the app. For example,
foo.myapp.com/barcould be rewritten and rendered asmyapp.com/_sites/foo/bar, utilizing the routing structure within/routes/_sitesto render thefootenant's 'website' using the same codebase as the core app.This behavior can't be achieved within
entry.server.ts, since by the point that is called, Remix has already done up-front route matching on the original URL. Any userland code we could write to achieve this rewriting would have to be supplied to the server runtime directly.Currently, the only working alternative which enables URL rewrite-based multi-tenancy is to modify the platform-specific deployment entrypoint (i.e.
/api/index.jsfor Vercel, etc) to rewrite the URL before Remix gets it. But this only works in deployed environments (not locally), and would have to be rewritten for different deployment targets if you switch.CC: #1475
Why should this feature be included?
All reactions