Skip to content

Commit

Permalink
Remove pre-read FormData option (#40)
Browse files Browse the repository at this point in the history
  • Loading branch information
sergiodxa authored Jun 3, 2024
1 parent d211497 commit 41f5228
Show file tree
Hide file tree
Showing 3 changed files with 30 additions and 64 deletions.
18 changes: 0 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,21 +75,3 @@ export async function action({ context, request }: ActionArgs) {
});
}
```

## Passing a pre-read FormData object

Because you may want to do validations or read values from the FormData before calling `authenticate`, the FormStrategy allows you to pass a FormData object as part of the optional context.

```ts
export async function action({ context, request }: ActionArgs) {
let formData = await request.formData();
return await authenticator.authenticate("form", request, {
// use formData here
successRedirect: formData.get("redirectTo"),
failureRedirect: "/login",
context: { formData }, // pass pre-read formData here
});
}
```

This way, you don't need to clone the request yourself.
40 changes: 0 additions & 40 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,46 +73,6 @@ test("should pass the context to the verify callback", async () => {
expect(verify).toBeCalledWith({ form: body, context, request });
});

test("should prefer context.formData over request.formData()", async () => {
let body = new FormData();
body.set("email", "test@example.com");

let request = new Request("http://.../test", { body, method: "POST" });

let context = { formData: body };

let strategy = new FormStrategy<string>(async ({ form }) => {
return form.get("email") as string;
});

expect(
strategy.authenticate(request, sessionStorage, {
...options,
context,
}),
).resolves.toBe("test@example.com");
});

test("ignore context.formData if it's not an FormData object", async () => {
let body = new FormData();
body.set("email", "test@example.com");

let request = new Request("http://.../test", { body, method: "POST" });

let context = { formData: { email: "fake@example.com" } };

let strategy = new FormStrategy<string>(async ({ form }) => {
return form.get("email") as string;
});

expect(
strategy.authenticate(request, sessionStorage, {
...options,
context,
}),
).resolves.toBe("test@example.com");
});

test("should pass error as cause on failure", async () => {
verify.mockImplementationOnce(() => {
throw new TypeError("Invalid email address");
Expand Down
36 changes: 30 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ export class FormStrategy<User> extends Strategy<
options: AuthenticateOptions,
): Promise<User> {
try {
let form = await this.readFormData(request, options);
if (request.bodyUsed) throw new BodyUsedError();
let form = await request.clone().formData();
let user = await this.verify({ form, context: options.context, request });
return this.success(user, request, sessionStorage, options);
} catch (error) {
Expand Down Expand Up @@ -65,12 +66,35 @@ export class FormStrategy<User> extends Strategy<
);
}
}
}

private async readFormData(request: Request, options: AuthenticateOptions) {
if (options.context?.formData instanceof FormData) {
return options.context.formData;
}
/**
* Whenever you try to call `authenticator.authenticate("form", request)` and
* the request's body was already read before, the FormStrategy will throw this
* error.
*
* To fix it, ensure that you either move the logic that depends on the body to
* inside the strategy's `verify` callback, or clone the request before reading.
*
* @example
* let formData = await request.clone().formData()
* // do something with formData
* let user = await authenticator.authenticate("form", request);
*
* @example
* authenticator.use(
* new FormStrategy(async ({ form }) => {
* // do something with form here
* }),
* "login",
* );
*/
export class BodyUsedError extends Error {
name = "BodyUsedError";

return await request.formData();
constructor() {
super(
"Your request.body was already used. This means you called `request.formData()` or another way to read the body before using the Remix Auth's FormStrategy. Because FormStrategy needs to read the body, ensure that you either don't read it yourself by moving your logic to the strategy callback or clone the request before reading the body with `request.clone().formData()`.",
);
}
}

0 comments on commit 41f5228

Please sign in to comment.