Resend email confirmation redirects with malformed URL parameters

I'm using supabase-js. When user clicks on an expired email verification URL it gets redirected to my site with a URL param error_code=otp_expired (e.g. mysite.com/email-confirmed?error_code=otp_expired&<other params>. I can detect that and allow user to resend the verification email using supabase.auth.resend function. How ever when that OTP expires supabase backend redirects me to the correct endpoint but with incorrect URL params (rather malformed URL). The URL looks like mysite.com/email-confirmed#error_code=otp_expired&<other params>. Yes there's a hash instead of a question mark. Got any idea why's that happening? Simplified code example
await supabase.auth.signUp({
email: 'me@example.dev',
password: 'passwd',
options: {
emailRedirectTo: 'mysite.com/email-confirmed'
}
})

// wait for OTP to expire
// click on the email link -> redirected to mysite.com/email-confirmed?error_code=otp-expired&<more params>
// let's resend the verification email

await supabase.auth.resend({
email: 'me@example.dev',
type: 'signup',
options: {
emailRedirectTo: 'mysite.com/email-confirmed'
}
})
// wait for the second OTP to expire
// click on the email link -> redirected to mysite.com/email-confirmed#error_code=otp-expired&<more params>
// see the hash ^
await supabase.auth.signUp({
email: 'me@example.dev',
password: 'passwd',
options: {
emailRedirectTo: 'mysite.com/email-confirmed'
}
})

// wait for OTP to expire
// click on the email link -> redirected to mysite.com/email-confirmed?error_code=otp-expired&<more params>
// let's resend the verification email

await supabase.auth.resend({
email: 'me@example.dev',
type: 'signup',
options: {
emailRedirectTo: 'mysite.com/email-confirmed'
}
})
// wait for the second OTP to expire
// click on the email link -> redirected to mysite.com/email-confirmed#error_code=otp-expired&<more params>
// see the hash ^
5 Replies
inder
inder3w ago
I've tested on both local and hosted supabase and this is the url I get. Both start with hash
http://localhost:5173/#error=access_denied&error_code=otp_expired&error_description=Email+link+is+invalid+or+has+expired
http://localhost:5173/#error=access_denied&error_code=otp_expired&error_description=Email+link+is+invalid+or+has+expired
http://127.0.0.1:3000/#error=access_denied&error_code=otp_expired&error_description=Email+link+is+invalid+or+has+expired
http://127.0.0.1:3000/#error=access_denied&error_code=otp_expired&error_description=Email+link+is+invalid+or+has+expired
silentworks
silentworks3w ago
Can you show what your createClient looks like and also where it is being imported from?
vitekma6
vitekma6OP3w ago
I have a wrapper function for that but here it is:
// serverClient.ts
import { Database } from '@/types/supabase'
import { createServerClient as _createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export async function createServerClient() {
const cookieStore = await cookies()

return _createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
)
} catch {
// The setAll method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
},
}
)
}

// auth_actions.ts
export async function resendAuthEmail(email: string) {
const supabase = await createServerClient()

const redirect = ${process.env.NEXT_PUBLIC_BASE_URL}/email-confirmed?email=${email}

const { error } = await supabase.auth.resend({
type: 'signup',
email: email,
options: {
emailRedirectTo: redirect
}
})

// some error checks...
}
// serverClient.ts
import { Database } from '@/types/supabase'
import { createServerClient as _createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export async function createServerClient() {
const cookieStore = await cookies()

return _createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
)
} catch {
// The setAll method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
},
}
)
}

// auth_actions.ts
export async function resendAuthEmail(email: string) {
const supabase = await createServerClient()

const redirect = ${process.env.NEXT_PUBLIC_BASE_URL}/email-confirmed?email=${email}

const { error } = await supabase.auth.resend({
type: 'signup',
email: email,
options: {
emailRedirectTo: redirect
}
})

// some error checks...
}
silentworks
silentworks3w ago
Remove your query param (?email=${email}) from the emailRedirectTo. Also you should get the error in a query string since you are using the ssr library.
vitekma6
vitekma6OP3w ago
I've already tried removing the email query param and it did not help.

Did you find this page helpful?