Skip to content

Commit

Permalink
Add Sec-Fetch utils (#257)
Browse files Browse the repository at this point in the history
  • Loading branch information
sergiodxa authored Oct 13, 2023
1 parent 7138d81 commit c799523
Show file tree
Hide file tree
Showing 3 changed files with 188 additions and 1 deletion.
74 changes: 74 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ module.exports = {
],
};
```

If you're not sure if your app uses ESM or CJS, check if you have `serverModuleFormat` in your `remix.config.js` file to know.

In case you don't have one, if you're using Remix v1 it will be CJS and if you're using Remix v2 it will be ESM.
Expand Down Expand Up @@ -1921,6 +1922,79 @@ export async function action({ request }) {
}
```

### Sec-Fetch Parsers

The `Sec-Fetch` headers include information about the request, e.g. where is the data going to be used, or if it was initiated by the user.

You can use the `remix-utils/sec-fetch` utils to parse those headers and get the information you need.

```ts
import {
fetchDest,
fetchMode,
fetchSite,
isUserInitiated,
} from "remix-utils/sec-fetch";
```

#### Sec-Fetch-Dest

The `Sec-Fetch-Dest` header indicates the destination of the request, e.g. `document`, `image`, `script`, etc.

If the value is `empty` it means it will be used by a `fetch` call, this means you can differentiate between a request made with and without JS by checking if it's `document` (no JS) or `empty` (JS enabled).

```ts
import { fetchDest } from "remix-utils/sec-fetch";

export async function action({ request }: ActionFunctionArgs) {
let data = await getDataSomehow();

// if the request was made with JS, we can just return json
if (fetchDest(request) === "empty") return json(data);
// otherwise we redirect to avoid a reload to trigger a new submission
return redirect(destination);
}
```

#### Sec-Fetch-Mode

The `Sec-Fetch-Mode` header indicates how the request was initiated, e.g. if the value is `navigate` it was triggered by the user loading the page, if the value is `no-cors` it could be an image being loaded.

```ts
import { fetchMode } from "remix-utils/sec-fetch";

export async function loader({ request }: LoaderFunctionArgs) {
let mode = fetchMode(request);
// do something based on the mode value
}
```

#### Sec-Fetch-Site

The `Sec-Fetch-Site` header indicates where the request is being made, e.g. `same-origin` means the request is being made to the same domain, `cross-site` means the request is being made to a different domain.

```ts
import { fetchSite } from "remix-utils/sec-fetch";

export async function loader({ request }: LoaderFunctionArgs) {
let site = fetchSite(request);
// do something based on the site value
}
```

#### Sec-Fetch-User

The `Sec-Fetch-User` header indicates if the request was initiated by the user, this can be used to differentiate between a request made by the user and a request made by the browser, e.g. a request made by the browser to load an image.

```ts
import { isUserInitiated } from "remix-utils/sec-fetch";

export async function loader({ request }: LoaderFunctionArgs) {
let userInitiated = isUserInitiated(request);
// do something based on the userInitiated value
}
```

## Author

- [Sergio Xalambrí](https://sergiodxa.com)
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"./honeypot/server": "./build/server/honeypot.js",
"./honeypot/react": "./build/react/honeypot.js",
"./csrf/server": "./build/server/csrf.js",
"./csrf/react": "./build/react/authenticity-token.js"
"./csrf/react": "./build/react/authenticity-token.js",
"./sec-fetch": "./build/server/sec-fetch.js"
},
"sideEffects": false,
"scripts": {
Expand Down
112 changes: 112 additions & 0 deletions src/server/sec-fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { z } from "zod";
import { getHeaders } from "./get-headers.js";

const FetchDestSchema = z.enum([
"audio",
"audioworklet",
"document",
"embed",
"empty",
"font",
"frame",
"iframe",
"image",
"manifest",
"object",
"paintworklet",
"report",
"script",
"serviceworker",
"sharedworker",
"style",
"track",
"video",
"worker",
"xslt",
]);

export type FetchDest = z.output<typeof FetchDestSchema>;

/**
* Returns the value of the `Sec-Fetch-Dest` header.
*
* The `Sec-Fetch-Dest` header indicates the destination of the request and
* can be used to know if the request came from a `fetch` call or not, among
* other things.
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-Fetch-Dest
* @example
* // Detect if the request came from a fetch call and return
* // json, otherwise return a redirect
* export async function action({ request }: ActionFunctionArgs) {
* let dest = fetchDest(request);
* if (dest === "empty") return json(data)
* return redirect(destination)
* }
*/
export function fetchDest(request: Request): FetchDest | null;
export function fetchDest(headers: Headers): FetchDest | null;
export function fetchDest(input: Request | Headers): FetchDest | null {
let header = getHeaders(input).get("Sec-Fetch-Dest");
let result = FetchDestSchema.safeParse(header);
if (result.success) return result.data;
return null;
}

const FetchModeSchema = z.enum([
"cors",
"navigate",
"no-cors",
"same-origin",
"websocket",
]);

export type FetchMode = z.output<typeof FetchModeSchema>;

/**
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-Fetch-Mode
*/
export function fetchMode(request: Request): FetchMode | null;
export function fetchMode(headers: Headers): FetchMode | null;
export function fetchMode(input: Request | Headers): FetchMode | null {
let headers = getHeaders(input).get("Set-Fetch-Mode");
let result = FetchModeSchema.safeParse(headers);
if (result.success) return result.data;
return null;
}

const FetchSiteSchema = z.enum([
"cross-site",
"same-origin",
"same-site",
"none",
]);

export type FetchSite = z.output<typeof FetchSiteSchema>;

/**
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-Fetch-Site
*/
export function fetchSite(request: Request): FetchSite | null;
export function fetchSite(headers: Headers): FetchSite | null;
export function fetchSite(input: Request | Headers): FetchSite | null {
let headers = getHeaders(input).get("Set-Fetch-Site");
let result = FetchSiteSchema.safeParse(headers);
if (result.success) return result.data;
return null;
}

const FetchUserSchema = z.literal("?1");

export type FetchUser = z.output<typeof FetchUserSchema>;

/**
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-Fetch-User
*/
export function isUserInitiated(request: Request): boolean;
export function isUserInitiated(headers: Headers): boolean;
export function isUserInitiated(input: Request | Headers): boolean {
let headers = getHeaders(input).get("Set-Fetch-User");
let result = FetchUserSchema.safeParse(headers);
if (result.success) return true;
return false;
}

0 comments on commit c799523

Please sign in to comment.