before_user_created Auth Hook Returns "Invalid payload sent to hook" Error When Rejecting User Creation
Bug report
- [x] I confirm this is a bug with Supabase, not with my own application.
- [x] I confirm I have searched the Docs, GitHub Discussions, and Discord.
Describe the bug
The before_user_created auth hook returns an "Invalid payload sent to hook" error when attempting to reject user account creation with a properly formatted HTTP 400 response. Instead of processing the rejection with the custom error message, Supabase returns an unexpected failure error.
To Reproduce
Steps to reproduce the behavior:
- Set up a
before_user_createdauth hook in your Supabase project dashboard - Configure it to point to a custom API endpoint (e.g., using ngrok for local development)
- Create an endpoint that conditionally rejects signups with this response:
return response .status(400) .set('Content-Type', 'application/json') .json({ error: { message: 'Signups from this email domain are not allowed.', http_code: 400, }, }); - Attempt to create a user account that should trigger the rejection logic
- Observe the error response from Supabase
Expected behavior
When the auth hook returns HTTP 400 with the proper error payload format, Supabase should:
- Prevent the account creation
- Return the custom error message specified in the hook response
- Handle the rejection gracefully without throwing an "Invalid payload" error
Screenshots
Error response received:
{
"code": "unexpected_failure",
"message": "Invalid payload sent to hook"
}
System information
- OS: [Various - issue occurs on server side]
- Browser: N/A (affects API calls)
- Version of supabase-js: NA
- Version of Node.js: NA
Additional context
- The API endpoint is successfully receiving requests from Supabase (confirmed via server logs)
- The API responds with the exact JSON format as documented
- When returning HTTP 200 with an empty body, account creation proceeds without issues
- This suggests the hook connectivity is working, but error handling for HTTP 400 responses is broken
- The same issue has been reported by other users (@Entropei)
- Hook URL is accessible and properly configured in the Supabase dashboard
The problem specifically occurs when trying to reject user creation - the success path works fine, but the rejection path fails with an unhelpful error message that doesn't match the documentation.
The documentation is inaccurate. The JSON payload for a rejection requires a top-level message and a top-level http_code, in addition to the error object:
{
"message": "Signups from this email domain are not allowed.",
"http_code": 400,
"error": {
"message": "Signups from this email domain are not allowed."
}
}
Please update your API endpoint to return this structure. Your response logic should look something like this:
return response
.status(400) // Or another 4xx code
.set('Content-Type', 'application/json')
.json({
message: 'Signups from this email domain are not allowed.',
http_code: 400,
error: {
message: 'Signups from this email domain are not allowed.'
}
});
Let me know if it works; I can update the documentation to show the same.
Hello, thank you for you swift answer !
Still not working unfortunately, I'm still having the exact same error as before. I can see in my ngrok dashboard, that my API is responding with
{
"message": "Signups from this email domain are not allowed.",
"http_code": 400,
"error": {
"message": "Signups from this email domain are not allowed."
}
}
edit: typo
I did a little dig, here's what I found out from the docs; It is working properly:
Currently there is support for only the following status codes mentioned in table:
If any other status code is returned, then it says the following:
{
"code": 500,
"error_code": "unexpected_failure",
"msg": "Unexpected status code returned from hook: 409",
"error_id": "9807cac3f544ac6d-MAA"
}
and from the image above, it seems returning 400 is treated as internal server error from the API side. So it is working as expected.
I tried mocking a 429 response(too many requests) and it returned the following:
{
"code": 500,
"error_code": "unexpected_failure",
"msg": "Service currently unavailable due to hook",
"error_id": "9807cbb874857f44-MAA"
}
It should actually re surface the custom error message you pass from your API, which it is not doing now. Hopefully a fix comes up!
Thank you very much for your help, mate! Indeed, for now, the finality is what we need: if we return 200, the account is created; if we return 400, the account is not created. We just need the custom error to bubble up—it's a detail, and I'm sure it will be resolved soon.
Looks like this is the problem in supabase/auth: https://github.com/supabase/auth/blob/01ebce1bf01b563105d653ff168a16e72c12d481/internal/hooks/hookshttp/hookshttp.go#L237
Please let me know when it's fixed.
I think this issue was labeled as external, but it seems to be a bug.
I also stumbled into this problem. I want to to restrict sign ups in my application to company domains.
I followed this tutorial to set everything up. The before_user_created hooks calls the edge function successfully, but no matter what I try, i cannot return the custom error message back to the client.
I always get
{ "__isAuthError": true, "name": "AuthApiError", "status": 500, "code": "unexpected_failure" }
I also found this information in the docs
403, 400 | Treated as Internal Server Errors and return a 500 Error Code
So i think the problem is that my edge function successfully returns am error code 403 which then causes supabase to return an error code 500 which also removes any custom message I want to sent to the client to inform my users that they should use their work email address instead.
I don't understand why the functionality is that way. If i want to sent a custom error message from the before_user_create hook this is basically not possible, because any error is returned as a arbitrary 500 error.
Does someone how to fix this?
Transferred from supabase repo.
I'm facing the same issue. I'm trying to use edge function to block temporary emails from signing up with a custom error message but on the client supabase auth always returns 500 with message "Invalid payload sent to hook"
I ended up using an edge function, that I call immediately after the signin request resolves successfully. In this edge function I check the email domain of the new users. If the email is not allowed, I am deleting the auth user and return the custom error message. From the users perspective I show an loading state during the sign in request AND the edge function call. Only after both resolve I show the error message or redirect to, in my case the profile page.
The issue seems to be originating from this condition https://github.com/supabase/auth/blob/a6530d54224e5fc00995c47c2e272a9ff1adf9cb/internal/hooks/hookshttp/hookshttp.go#L235C3-L237C36
Should we just parse the body and pass on the custom error message here?
+1. Please fix the bug and update documentation.
I currently use the following workaround and catch the 422 response on the client side. Not ideal but works.
return new Response(
JSON.stringify({
error: {
message: '.. rejected message',
http_code: 422,
},
}),
{ status: 422, headers: { 'Content-Type': 'application/json' } }
);
The bug is still present ... Any updates ?
I faced them same issue while i signup and try sending supabase emails into my notifuse service which is my email integration
Hi everyone, my apologies for this oversight. So how it works today is as described in https://github.com/supabase/auth/issues/2235#issuecomment-3472037975 - it's strictly based on status which does not match the docs for the before user created hook.
@mklb @Triskae I've changed this in auth#2380 and will follow up with a docs update asap.
Package hookerrors was meant to introduce consistent error handling across hookshttp and hookspgfunc. However it was not being used within the hookshttp package. This change fixes that to have one consistent error mechanism supported across all hooks. For http hooks specifically there is an error in the supabase docs that I will resolve in a follow up pr. The status code from the invoked hook should indicate the response status only for the auth server. This means a response of 500 will be treated as a failed invocation of the hook and the response body will not be ready. Responses will only be read when the status code is 200 or 202. If so it will read the body, if it is the body will be checked by the hookserrors package for an error object. If present it will be propagated to the original client.
I know this API is a little inconsistent, there is some hesitation to change the original hooks due to backwards compatibility. But after my PR is merged there is at least a single way to do error handling across all hook types if you choose to use that style.
Here is an example of how to do this after the next auth release drops (v2.187.0)
return response
// Must set status code to 200 or 202 for this style of error handling
.status(200)
// Must set status code
.set('Content-Type', 'application/json')
// If an error object is present
.json({
error: {
// Must contain a message, without this the http_code will not work (legacy behavior 😅)
message: 'This error message propagates to the user',
// The http_code is optional, if omitted it's turned into a 500 (legacy behavior 😅)
http_code: 400,
}
});
Note: I would limit your status codes to 4XX range, in the future I would like to avoid returning 500 codes, but for now due to BC it will remain like this.