๐บ๏ธ Route Middleware #7642
Replies: 24 comments 42 replies
-
Where is |
Beta Was this translation helpful? Give feedback.
-
Overall, my yardstick for a good middleware implementation would be one that can replace With that in mind, this is how I would intuitively write the last example: function redirectsMiddleware({ request, next }: Middleware) {
// attempt to handle the request
try {
return response = await next();
} catch (error) {
if (isResponse(error) && error.status === 404) {
// if it's a 404, check the CMS for a redirect, do it last
// because it's expensive
let cmsRedirect = await checkCMSRedirects(request.url);
if (cmsRedirect) {
throw redirect(cmsRedirect, 302);
}
}
}
} The 2 changes I made there are:
3 top-of-mind questions:
|
Beta Was this translation helpful? Give feedback.
-
In conjunction with #7640, it seems like a |
Beta Was this translation helpful? Give feedback.
-
If the route throws a I think it would be simplest if the |
Beta Was this translation helpful? Give feedback.
-
I am wondering, all the middleware run before loader AND action? or just loader? |
Beta Was this translation helpful? Give feedback.
-
For Shopify Apps we have an auth method like this:
What's nice about this is:
With Middleware, I'm imagining something like this:
Which is really nice because auth get's even simpler, but I have 2 questions about the other points:
|
Beta Was this translation helpful? Give feedback.
-
I started off writing a comment on what I would like it to look like and further I explored the other RFC's the further it cut my original comment and now I'm out of words unfortunately. This is simply amazing, also separating the context from the middleware is great. I am just want to confirm the following:
|
Beta Was this translation helpful? Give feedback.
-
I like this middleware RFC.
|
Beta Was this translation helpful? Give feedback.
-
Is it intentional to not also allow middleware to modify requests or is this just the wrong wording. |
Beta Was this translation helpful? Give feedback.
-
With this, would it be possible to set and access route-level metadata in Middleware? It's a big of an edge case, but sometimes I wanna to toggle or affect a middleware's behavior for a specific route (for a simple example, maybe skip auth). |
Beta Was this translation helpful? Give feedback.
-
This is great and almost exactly what I was hoping for. I've been struggling to figure out how the middleware can be used for validation and model binding though. I think if the current route For example, using // validation.server.ts
async function validation(props) {
const { request, matches, next } = props;
const route = getTargetMatch(matches);
if (route?.action && !route.schema)
throw new Error("Routes with actions must have a validation schema exported.");
if (route?.schema) {
const result = await getValidatedFormData(request, route.schema);
if (!result.success) throw new Response(result.errors, { status: 400 });
// model bind the valid form data to the action 'data' prop
props.data = result.data; // Object.assign(props, data)?
}
return next();
}
// routes/hello.tsx
export const middleware = [validation];
export const schema = z.object({
a: z.number(),
b: z.string(),
});
export function action({ data }): DataFunctionArgs<typeof schema> {
return json({ message: "hello from action!", a: data.a, b: data.b });
} By passing the middleware props object through to the action it allows dependency injection from middleware, this same support can be utilized by the default server context and session middleware since they inject // middleware pipeline
exceptions -> context -> routing -> validation -> session -> resource -> loaders -> action Adding // /middleware/api.server.ts
async function api({ request, matches, next }) {
for (const match of matches) {
if (match.route.default) continue;
const method = request.method.toLowerCase();
match.route.loader = (props) => match.route['get'](props); // get convention.
match.route.action = (props) => match.route[method](props); // post, put, patch conventions.
}
return next();
} Automatically wrapping loaders with http timing headers could be another use case: // /middleware/timing.server.ts
async function timing({ matches, next }) {
for (const match of matches) {
match.route.loader = (props) => time(() => match.route.loader(props));
}
return next();
} The possibilities are endless with access to matches but here's one more interesting example of a team's route config convention that wraps the default component export to make a route client only. Similar could be used for dev tooling wrapping route boundaries @AlemTuzlak. // /middleware/config.tsx
async function config({ matches, next }) {
for (const match of matches) {
if(match.route.config?.clientOnly) {
match.route.default = () => (<ClientOnly>{() => match.route.default}</ClientOnly>);
}
}
return next();
}
// routes/hello.tsx
export const middleware = [config];
export const config = { clientOnly: true }; This would also require middleware to run client-side, so i'm hoping |
Beta Was this translation helpful? Give feedback.
-
I think there should be parallel |
Beta Was this translation helpful? Give feedback.
-
This looks amazing! When is it coming? ๐ |
Beta Was this translation helpful? Give feedback.
-
Super excited for this. I'm currently implementing a much more cumbersome middleware stack in server.js and would much rather have support within Remix. ๐ |
Beta Was this translation helpful? Give feedback.
-
Isn't the Response object read only? If the only way to modify a response is to throw, which skips all the other middleware, then it's not very middleware-like. If I add middleware to:
Then every time the cache hits, the response time middleware will be skipped. I'd prefer to see an approach similar to koajs, where the request and response objects are abstracted. Then it becomes minimal effort to do things such as append a header to a response, without needing to rebuild the whole response. It makes sense for loaders to return a Response object when there's no middleware that could potentially modify it afterwards. With middleware, it feels more logical to only construct the final Response object after all middleware. In express, their routes directly set the HTTP response, which makes it impossible for middleware to run after a route without monkey-patching the response. The compression library (which remix-express uses) does it. Their own response-time middleware does it. The same for any express cache middleware. If remix wants to modify responses in middleware, then Sometimes developers don't have access to or permission to use a CDN, which means they can't cache pages effectively. My ideal for Remix middleware would be to implement my own LRU-cache in middleware without having to create my own remix-run/{adapter}. I imagine that this might be a huge breaking change, but I feel like the end result would make remix way more extensible. |
Beta Was this translation helpful? Give feedback.
-
Would ya'll consider supporting middleware (or context, from the context RFC) in the The main use case here vs the middleware in the root route, is to work with the request before the default request handler in Here's a more concrete example of what I'd love to be able to do: // entry.server.tsx
const storage = new AsyncLocalStorage<{ tanstackQueryClient: QueryClient }>();
function initReqCtx({ request, next }: Middleware) {
const tanstackQueryClient = new QueryClient();
// can access the things in storage from loaders, and in handleRequest below
return storage.run({ tanstackQueryClient }, () => next())
}
export const middleware = [initReqCtx];
export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
) {
const stream = await renderToReadableStream(<RemixServer context={remixContext} url={request.url} />);
const { tanstackQueryClient } = storage.getStore();
/**
* HERE! Not including the code, but here is where I pipe the stream through some transfomers
* to inject script snippets from libraries like tanstack query, to pass their cached data to the client
*
* Basically the use case is to share contextual objects with the app and loaders, and then have access
* to those objects in the root request handler so that we can manipulate the response stream as needed.
*/
return new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
});
} |
Beta Was this translation helpful? Give feedback.
-
How would this middleware play with the route loader? |
Beta Was this translation helpful? Give feedback.
-
Having struggled mightily with session race conditions, I am really hoping single fetch + server context + route middleware will solve many of the problems described in #5647. It seems pretty straightforward that once we enable single fetch, if we What I'm less sure of is how we'll safely
I can't decide yet whether that last part is an API smell or excellent design; but if all of the above is roughly correct, then it sounds great to me and I look forward to the improvements. |
Beta Was this translation helpful? Give feedback.
-
I've implemented the Remix Middleware spec in user-land (no changes to Remix core). It also includes some enhancements. This currently requires Vite, and it supports HMR on middleware. It uses my Vite+Express plugin (a new version is coming out soon). Also requires Single Fetch enabled to ensure middleware works the same for both server and client navigations. I added a couple of additional arguments to the Middleware function arguments. export type MiddlewareFunctionArgs = {
request: Request
context: AppLoadContext
params: Record<string, string>
matches: ReturnType<typeof matchRoutes>
next: () => Promise<Response>
} The const user = await getUser(request)
context.user = user In addition to context, it includes the You can have middleware functions on any route. All routes matching the current URL (same as loaders) with middleware will be called in sequence from the root to the leaf route. You must return the To inspect or modify the response: const response = await next()
// do something with the status
if (response.status === '404') {
throw redirect('/somewhere-else')
}
// you can also mutate the response (only headers for now)
response.headers.set('x-middleware', 'from middleware') Example// root.tsx
// export list of middleware functions
export const middleware = [root_mw1, root_mw2]
async function root_mw1({ request, context, next }: MiddlewareFunctionArgs) {
console.log('[MIDW] root_mw1 enter')
// add headers to the request for downstream middleware or loader/action to use
request.headers.append('x-middleware', 'root1')
context.root1 = 'added by root1 middleware'
const response = await next()
response.headers.append('x-return-middleware', 'root1 middleware')
console.log(
'[MIDW] root_mw1 response headers',
Object.fromEntries(
Array.from(response.headers).filter(([key]) => key.startsWith('x-')),
),
)
return response
}
async function root_mw2({ request, context, next }: MiddlewareFunctionArgs) {
console.log('[MIDW] root_mw2 enter')
request.headers.append('x-middleware', 'root2')
context.root2 = 'added by root2 middleware'
const response = await next()
response.headers.append('x-return-middleware', 'root2 middleware')
console.log(
'[MIDW] root_mw2 response headers',
Object.fromEntries(
Array.from(response.headers).filter(([key]) => key.startsWith('x-')),
),
)
return response
}
// routes/counter.tsx
export const middleware = [counter_mw3]
async function counter_mw3({ request, context, next }: MiddlewareFunctionArgs) {
console.log('[MIDW] counter_mw3 enter')
request.headers.set('x-counter', 'counter middleware')
context.counter = { message: 'added by counter middleware' }
let response = await next()
response.headers.append('x-return-middleware', 'counter middleware')
console.log(
'[MIDW] counter_mw3 response headers',
Object.fromEntries(
Array.from(response.headers).filter(([key]) => key.startsWith('x-')),
),
)
return response
}
export async function loader({ request, context }: LoaderFunctionArgs) {
// request headers includes those added by middleware
// context includes data appended to context
console.log(
'[LOADER] request header',
Object.fromEntries(
Array.from(request.headers).filter(([key]) => key.startsWith('x-')),
),
)
console.log('[LOADER] context', context)
return { count: await db.getCount() }
} OutputGET /counter 200 - - 15.922 ms
# Calling middleware chain
[INFO] calling middleware root_mw1
[MIDW] root_mw1 enter
[INFO] calling middleware root_mw2
[MIDW] root_mw2 enter
[INFO] calling middleware counter_mw3
[MIDW] counter_mw3 enter
# Finally, call Remix route handler
[INFO] calling remix handler
[LOADER] request header { 'x-counter': 'counter middleware', 'x-middleware': 'root1, root2' }
[LOADER] context {
root1: 'added by root1 middleware',
root2: 'added by root2 middleware',
counter: { message: 'added by counter middleware' }
}
# Got Remix Response
[INFO] response status from remix handler 200
# Return up the middleware chain modifying headers
[MIDW] counter_mw3 response headers { 'x-return-middleware': 'counter middleware' }
[MIDW] root_mw2 response headers { 'x-return-middleware': 'counter middleware, root2 middleware' }
[MIDW] root_mw1 response headers {
'x-return-middleware': 'counter middleware, root2 middleware, root1 middleware'
} |
Beta Was this translation helpful? Give feedback.
-
Note that we've added a |
Beta Was this translation helpful? Give feedback.
-
Is this coming to RR v7? |
Beta Was this translation helpful? Give feedback.
-
Any timeline when middleware might be coming ? |
Beta Was this translation helpful? Give feedback.
-
I think we are all waiting for this feature ... Note: My project needs include running on an express server and implementing the authentication in a middleware.
npx create-remix@latest --template remix-run/remix/templates/express
// app/sessions.ts
import { createCookieSessionStorage } from "@remix-run/node"; // or cloudflare/deno
export type SessionData = {
userId: string;
};
export type SessionFlashData = {
error: string;
};
declare module '@remix-run/node' {
interface AppLoadContext {
session: Session<SessionData, SessionFlashData>
}
}
const { getSession, commitSession, destroySession } =
createCookieSessionStorage<SessionData, SessionFlashData>(
{
// a Cookie from `createCookie` or the CookieOptions to create one
cookie: {
name: "__session",
// all of these are optional
domain: "remix.run",
// Expires can also be set (although maxAge overrides it when used in combination).
// Note that this method is NOT recommended as `new Date` creates only one date on each server deployment, not a dynamic date in the future!
//
// expires: new Date(Date.now() + 60_000),
httpOnly: true,
maxAge: 60,
path: "/",
sameSite: "lax",
secrets: ["s3cret1"],
secure: true,
},
}
);
export { getSession, commitSession, destroySession };
// app/auth-middleware.ts
import { Session } from '@remix-run/node'
import { Request, Response, NextFunction } from 'express'
import { getSession } from './sessions'
const LOGIN_PATHNAME = '/login'
export async function authMiddleware(
req: Request,
res: Response,
next: NextFunction,
) {
const cookieHeader = req.headers?.cookie
const session = await getSession(cookieHeader)
res.locals.session = session
if (req.path !== LOGIN_PATHNAME && !session.id) {
return res.status(401).redirect(LOGIN_PATHNAME)
}
next()
}
// server.js
// .....
// we add the session to be available in the remix context ... therefore available in the actions and loaders
const remixHandler = createRequestHandler({
build: viteDevServer
? () => viteDevServer.ssrLoadModule("virtual:remix/server-build")
: await import("./build/server/index.js"),
getLoadContext: (_, res) => {
return {
session: res.locals.session,
}
},
});
// .....
app.use(express.static("build/client", { maxAge: "1h" }));
app.use(morgan("tiny"));
// add the middleware
app.use(authMiddleware)
// handle SSR requests
app.all("*", remixHandler);
// ...
// app/routes/login.tsx
// ...
export async function action({ request, context }: LoaderFunctionArgs) {
const { session } = context
const body = await request.formData()
const username = String(body.get('username'))
const password = String(body.get('password'))
const user = {} // get the user from the database
if (!user) {
return json({ user: null }, { status: 401 })
}
createSession(user, session) // save the user in the session
return redirect('/', {
headers: {
'Set-Cookie': await commitSession(session, { // commit the session as cookie
maxAge: 60 * 60 * 24 * 30, // 30 days
}),
},
})
}
// ... // app/routes/home.tsx
// ...
export async function loader({ context }: LoaderArgs) {
const { session } = context
const user = session.get('userId') // get the user from the session
return json({ user })
}
// ... |
Beta Was this translation helpful? Give feedback.
-
I want choice Remix or Qwik City as stack for future project and exists middleware tech is very important for me. Why Remix doesn't support middleware now? For example, middleware NextJS can't exec request to database pg, this also don't comfortable! |
Beta Was this translation helpful? Give feedback.
-
Route middleware are functions that can inspect the request before the app runs and modify the response after the app is ready to respond.
Proposal
This supersedes remix-run/react-router#9566.
Middleware are functions you can add to a
middleware
export of a route module:Middleware are called in the order they are defined in the array.
Future Flag
This feature depends on the single fetch future flag:
Signature
Throwing Responses
Middleware can throw a response at any time. The response will be returned immediately and no other middleware will be called.
Examples
Logging Requests and Response Times
This one makes sense as the first middleware in root.tsx
Require Authentication
This middleware redirects to the login page if there is no user in the session.
Now an entire section of your app can be protected with a single middleware:
Inspecting Responses
This example inspects the response for 404s and then checks a CMS for any redirects configured by the site admins. The redirects are checked last so that every request isn't slowed down by the CMS check.
React Router w/o Remix
Not sure what this looks like for React Router yet, might simply be another property on routes
{ middleware: [requireAuth] }
Beta Was this translation helpful? Give feedback.
All reactions