Replies: 22 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(...)
}; |
Beta Was this translation helpful? Give feedback.
-
@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(...);
}); |
Beta Was this translation helpful? Give feedback.
-
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! |
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
There is no need for a middleware concept for multi-tenancy apps (see my comment on #1337). |
Beta Was this translation helpful? Give feedback.
-
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 |
Beta Was this translation helpful? Give feedback.
-
@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. |
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
+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? |
Beta Was this translation helpful? Give feedback.
-
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 🤔😅 |
Beta Was this translation helpful? Give feedback.
-
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";
|
Beta Was this translation helpful? Give feedback.
-
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:
|
Beta Was this translation helpful? Give feedback.
-
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
|
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
Is there a way for us to check progress? Is it currently possible to intercept all calls to |
Beta Was this translation helpful? Give feedback.
-
Hi any update on plans for middleware? |
Beta Was this translation helpful? Give feedback.
-
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/bar
could be rewritten and rendered asmyapp.com/_sites/foo/bar
, utilizing the routing structure within/routes/_sites
to render thefoo
tenant'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.js
for 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?
Beta Was this translation helpful? Give feedback.
All reactions