Skip to content

Commit

Permalink
feat(middleware): allow responseHeaders in middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
aralroca committed Oct 4, 2023
1 parent becf6ac commit 37035ee
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,36 @@ export default async function Layout({ children }: { children: JSX.Element }, {
```

The `fetch` is directly native and has no wrapper to control the cache. We recommend that you do not do the same `fetch` in several places, but use the [`context`](/docs/building-your-application/data-fetching/request-context) to store the data and consume it from any component.

## Response headers in layouts and pages

The `responseHeaders` function can be exported inside the `layout` and inside any `page`. In the same way that is possible to export it also in the [`middleware`](docs/building-your-application/routing/middleware).

All `responseHeaders` will be mixed in this order:

1. `middleware` response headers
2. `layout` response headers (can crush the middleware response headers)
3. `page` response headers (both middleware and layout response headers can be mixed).

```ts filename="middleware.ts" switcher
import { type RequestContext } from "brisa";

export function responseHeaders(
request: RequestContext,
responseStatus: number,
) {
return {
"Cache-Control": "public, max-age=3600",
"X-Example": "This header is added from layout",
};
}
```

```js filename="middleware.js" switcher
export function responseHeaders(request, responseStatus) {
return {
"Cache-Control": "public, max-age=3600",
"X-Example": "This header is added from layout",
};
}
```
46 changes: 23 additions & 23 deletions docs/02-building-your-application/01-routing/05-api-routes.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,15 @@ You can read the `Request` body using the standard Web API methods:

```ts filename="src/api/items/route.ts" switcher
export async function POST(request: RequestContext) {
const res = await request.json()
return new Response(JSON.stringify({ res }))
const res = await request.json();
return new Response(JSON.stringify({ res }));
}
```

```js filename="src/api/items/route.js" switcher
export async function POST(request) {
const res = await request.json()
return new Response(JSON.stringify({ res }))
const res = await request.json();
return new Response(JSON.stringify({ res }));
}
```

Expand All @@ -72,19 +72,19 @@ You can read the `FormData` using the standard Web API methods:

```ts filename="src/api/items/route.ts" switcher
export async function POST(request: RequestContext) {
const formData = await request.formData()
const name = formData.get('name')
const email = formData.get('email')
return new Response(JSON.stringify({ name, email }))
const formData = await request.formData();
const name = formData.get("name");
const email = formData.get("email");
return new Response(JSON.stringify({ name, email }));
}
```

```js filename="src/api/items/route.js" switcher
export async function POST(request) {
const formData = await request.formData()
const name = formData.get('name')
const email = formData.get('email')
return new Response(JSON.stringify({ name, email }))
const formData = await request.formData();
const name = formData.get("name");
const email = formData.get("email");
return new Response(JSON.stringify({ name, email }));
}
```

Expand Down Expand Up @@ -198,39 +198,39 @@ You can add the `Cache-Control` headers to the response. By default is not using
```ts filename="app/items/route.ts" switcher
export async function GET() {
const data = await getSomeData()
const res = new Response(JSON.stringify(data))
const data = await getSomeData();
const res = new Response(JSON.stringify(data));

res.headers.set("Cache-Control", "max-age=86400")
res.headers.set("Cache-Control", "max-age=86400");

return res
return res;
}
```
## Headers and Cookies
You can read headers and cookies from the [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and write headers and cookies to the [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) using Web APIs.
You can read headers and cookies from the [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and write headers and cookies to the [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) using Web APIs.
Example reading/writing cookies:
```ts filename="api/route.ts" switcher
import { type RequestContext } from "brisa";

export async function GET(request: RequestContext) {
const cookies = request.headers.get('cookie')
const res = new Response('Hello, Brisa!');
const cookies = request.headers.get("cookie");
const res = new Response("Hello, Brisa!");

if (cookies) {
res.headers.set('set-cookie', cookies)
res.headers.set("set-cookie", cookies);
}

return res
return res;
}
```
## Streaming
You can use the Web APIs to create a [stream](https://bun.sh/docs/api/streams) and then return it inside the `Response`:
You can use the Web APIs to create a [stream](https://bun.sh/docs/api/streams) and then return it inside the `Response`:
```ts
export async function GET() {
Expand All @@ -243,7 +243,7 @@ export async function GET() {
},
});

return new Response(stream) // Hello Brisa!
return new Response(stream); // Hello Brisa!
}
```
Expand Down
159 changes: 159 additions & 0 deletions docs/02-building-your-application/01-routing/06-middleware.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
---
title: Middleware
description: Learn how to use Middleware to run code before a request is completed.
---

Middleware allows you to run code before a request is completed. Then, based on the incoming request, you can modify the response by rewriting, redirecting, modifying the request or response headers, or responding directly.

Middleware runs before routes are matched. See [Matching Paths](#matching-paths) for more details.

## Convention

Use the file `middleware.ts` (or `.js`) inside the `src` folder of your project to define Middleware. Or inside `src/middleware/index.ts` (or `.js`).

## Example

```ts filename="middleware.ts" switcher
import { type RequestContext } from "brisa";

// This function can be without `async` if you are not using `await` inside
export default async function middleware({
i18n,
route,
headers,
}: RequestContext): Response | undefined {
const { locale } = i18n;
const isUserRoute = route?.name?.startsWith("/user/[username]");

if (isUserRout && !(await isUserLogged(headers))) {
return new Response("", {
status: 302,
headers: {
Location: `/${locale}/login`,
},
});
}
}
```

```js filename="middleware.js" switcher
// This function can be without `async` if you are not using `await` inside
export default async function middleware({ i18n, route, headers }) {
const { locale } = i18n;
const isUserRoute = route?.name?.startsWith("/user/[username]");

if (isUserRout && !(await isUserLogged(headers))) {
return new Response("", {
status: 302,
headers: {
Location: `/${locale}/login`,
},
});
}
}
```
Only is possible to access to `route` property inside `api routes` and `pages routes`. This is to support handling of dynamic routes, catch-all, etc in a simple way. In the case of `assets` you can look it up through the request:
```ts filename="middleware.ts" switcher
import { type RequestContext } from "brisa";

export default async function middleware(
request: RequestContext,
): Response | undefined {
const url = new URL(request.url);

if (url.pathname === "/favicon.svg") {
return new Response(
`
<svg>
<rect width="100" height="100" fill="red" />
</svg>
`,
{
headers: { "content-type": "image/svg+xml" },
},
);
}
}
```
```js filename="middleware.js" switcher
export default async function middleware(request) {
const url = new URL(request.url);

if (url.pathname === "/favicon.svg") {
return new Response(
`
<svg>
<rect width="100" height="100" fill="red" />
</svg>
`,
{
headers: { "content-type": "image/svg+xml" },
},
);
}
}
```
However, this is not the best way to serve assets. You can put the static files directly inside the `public` folder. More information [here](/building-your-application/optimizing/static-assets).
## Cookies & Headers
### On Request
Cookies are regular headers. On a `Request`, they are stored in the `Cookie` header.
```ts filename="middleware.ts" switcher
import { type RequestContext } from "brisa";

export default async function middleware(request: RequestContext) {
const cookies = request.headers.get("cookie");
const headers = request.headers.get("x-example");

// ... do something with cookies and headers
}
```
```js filename="middleware.js" switcher
export default async function middleware(request) {
const cookies = request.headers.get("cookie");
const headers = request.headers.get("x-example");

// ... do something with cookies and headers
}
```
### On Response
The `responseHeaders` function can be exported in the `middleware`, in the same way that you can do it inside `layout` and `pages`.
All responseHeaders will be mixed in this order:
1. `middleware` response headers
2. `layout` response headers (can crush the middleware response headers)
3. `page` response headers (both middleware and layout response headers can be mixed).
```ts filename="middleware.ts" switcher
import { type RequestContext } from "brisa";

export function responseHeaders(
request: RequestContext,
responseStatus: number,
) {
return {
"Cache-Control": "public, max-age=3600",
"X-Example": "This header is added from middleware",
};
}
```
```js filename="middleware.js" switcher
export function responseHeaders(request, responseStatus) {
return {
"Cache-Control": "public, max-age=3600",
"X-Example": "This header is added from middleware",
};
}
```
15 changes: 8 additions & 7 deletions src/cli/serve.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ if (!fs.existsSync(PAGES_DIR)) {
process.exit(1);
}

const customMiddleware = await importFileIfExists("middleware", ROOT_DIR);
const middlewareModule = await importFileIfExists("middleware", ROOT_DIR);
const customMiddleware = middlewareModule?.default;
let pagesRouter = getRouteMatcher(PAGES_DIR, RESERVED_PAGES);
let rootRouter = getRouteMatcher(ROOT_DIR);

Expand Down Expand Up @@ -122,7 +123,6 @@ console.log(
///////////////////////////////////////////////////////
////////////////////// HELPERS ///////////////////////
///////////////////////////////////////////////////////

async function handleRequest(req: RequestContext, isAnAsset: boolean) {
const locale = req.i18n.locale;
const url = new URL(req.finalURL);
Expand All @@ -131,11 +131,14 @@ async function handleRequest(req: RequestContext, isAnAsset: boolean) {
const isApi = pathname.startsWith(locale ? `/${locale}/api/` : "/api/");
const api = isApi ? rootRouter.match(req) : null;

req.route = isApi ? api?.route : route;

// Middleware
if (customMiddleware) {
const middlewareResponse = await Promise.resolve().then(() =>
customMiddleware(req),
);

if (middlewareResponse) return middlewareResponse;
}

Expand All @@ -148,9 +151,6 @@ async function handleRequest(req: RequestContext, isAnAsset: boolean) {
if (isApi && api?.route && !api?.isReservedPathname) {
const module = await import(api.route.filePath);
const method = req.method.toUpperCase();

req.route = api.route;

const response = module[method]?.(req);

if (response) return response;
Expand Down Expand Up @@ -192,20 +192,21 @@ async function responseRenderedPage({
const layoutPath = getImportableFilepath("layout", ROOT_DIR);
const layoutModule = layoutPath ? await import(layoutPath) : undefined;

req.route = route;

const pageElement = (
<PageLayout layoutModule={layoutModule}>
<PageComponent error={error} />
</PageLayout>
);

const middlewareResponseHeaders =
middlewareModule?.responseHeaders?.(req, status) ?? {};
const layoutResponseHeaders =
layoutModule?.responseHeaders?.(req, status) ?? {};
const pageResponseHeaders = module.responseHeaders?.(req, status) ?? {};
const htmlStream = renderToReadableStream(pageElement, req, module.Head);
const responseOptions = {
headers: {
...middlewareResponseHeaders,
...layoutResponseHeaders,
...pageResponseHeaders,
"transfer-encoding": "chunked",
Expand Down
8 changes: 4 additions & 4 deletions src/utils/import-file-if-exists/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@ describe("utils", () => {

describe("importFileIfExists", () => {
it("should return null if there is not a custom middleware", async () => {
const middleware = await importFileIfExists("middleware");
expect(middleware).toBeNull();
const middlewareModule = await importFileIfExists("middleware");
expect(middlewareModule).toBeNull();
});

it('should return custom middleware if "middleware.ts" exists', async () => {
path.join = () =>
join(import.meta.dir, "..", "..", "__fixtures__", "middleware");

const middleware = await importFileIfExists("middleware");
expect(middleware).toBeFunction();
expect(middleware?.default).toBeFunction();
});

it("should return null if there is not a custom i18n", async () => {
Expand All @@ -33,7 +33,7 @@ describe("utils", () => {
join(import.meta.dir, "..", "..", "__fixtures__", "i18n");

const i18n = await importFileIfExists("i18n");
expect(i18n).toEqual({
expect(i18n?.default).toEqual({
defaultLocale: "en",
locales: ["en", "fr"],
messages: {
Expand Down
Loading

0 comments on commit 37035ee

Please sign in to comment.