Skip to content

turbo-stream error when redirecting through an action while preserving headers #12850

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
Kylar13 opened this issue Jan 23, 2025 · 10 comments
Open

Comments

@Kylar13
Copy link

Kylar13 commented Jan 23, 2025

I'm using React Router as a...

framework

Reproduction

git clone https://github.com/Kylar13/react-router-redirect-bug.git
pnpm i && pnpm dev
Go to https://localhost:3000
Press Redirect button

System Info

System:
    OS: macOS 15.1.1
    CPU: (12) arm64 Apple M2 Pro
    Memory: 523.22 MB / 16.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 22.9.0 - /opt/homebrew/bin/node
    npm: 10.8.3 - /opt/homebrew/bin/npm
    pnpm: 9.15.0 - /opt/homebrew/bin/pnpm
    bun: 1.0.26 - /opt/homebrew/bin/bun
  Browsers:
    Edge: 132.0.2957.115
    Safari: 18.1.1
  npmPackages:
    @react-router/dev: ^7.1.1 => 7.1.3 
    @react-router/express: ^7.1.1 => 7.1.3 
    @react-router/node: ^7.1.1 => 7.1.3 
    react-router: ^7.1.1 => 7.1.3 
    vite: ^5.4.11 => 5.4.14

Used Package Manager

pnpm

Expected Behavior

The action redirects while preserving headers from the request.

Actual Behavior

The app crashes with the following error:

Error: Unable to decode turbo-stream response
    at fetchAndDecode (http://localhost:3000/node_modules/.vite/deps/chunk-3ENBBAXA.js?v=13107b6a:6854:11)
    at async http://localhost:3000/node_modules/.vite/deps/chunk-3ENBBAXA.js?v=13107b6a:6697:37
    at async http://localhost:3000/node_modules/.vite/deps/chunk-3ENBBAXA.js?v=13107b6a:6694:19
    at async http://localhost:3000/node_modules/.vite/deps/chunk-3ENBBAXA.js?v=13107b6a:4408:19
    at async callLoaderOrAction (http://localhost:3000/node_modules/.vite/deps/chunk-3ENBBAXA.js?v=13107b6a:4458:16)
    at async singleFetchActionStrategy (http://localhost:3000/node_modules/.vite/deps/chunk-3ENBBAXA.js?v=13107b6a:6693:16)
    at async callDataStrategyImpl (http://localhost:3000/node_modules/.vite/deps/chunk-3ENBBAXA.js?v=13107b6a:4368:17)
    at async callDataStrategy (http://localhost:3000/node_modules/.vite/deps/chunk-3ENBBAXA.js?v=13107b6a:3148:17)
    at async handleAction (http://localhost:3000/node_modules/.vite/deps/chunk-3ENBBAXA.js?v=13107b6a:2481:21)
    at async startNavigation (http://localhost:3000/node_modules/.vite/deps/chunk-3ENBBAXA.js?v=13107b6a:2362:26)```
@Kylar13 Kylar13 added the bug label Jan 23, 2025
@Kylar13
Copy link
Author

Kylar13 commented Jan 23, 2025

I didn't want to pollute the bug report so I'll post an explanation here:

I'm trying to redirect with the headers for Supabase Auth, which needs the headers to be there for a code validation that it handles by itself.

This is a concise version of my action:

export async function action({ request }: ActionFunctionArgs) {
  const supabase = createServerClient(request);
  const { data, error } = await supabase.auth.signInWithOAuth({
    provider: "google",
    options: {
      redirectTo: `${new URL(request.url).origin}/callback`,
      queryParams: {
        next: "/app",
      },
    },
  });

  if (error) {
    return { error: error.message };
  }
  if (data.url) {
    console.log("Redirecting to Google", { url: data.url });
    return redirect(data.url, { headers: request.headers });
  }
}

Without the headers the redirect works fine but Supabase Auth fails with an AuthApiError

~~As a sidenote, I tried to write the minimal repro as a test in the bug-integration-test but, after figuring out the command is actually pnpm test:integration bug-report-test and not pnpm bug-report-test, I started getting all sorts of errors from the parsing of the template to create the fixture.

It's probably a skill issue of mine but I'll leave the link here as well in case someone wants to take a look: https://github.com/Kylar13/react-router/commit/85a20834be8e0cd04b534d7b8c1b43e96ef2ccdc~~

EDIT: Figured out the bug-report thing and submitted a PR

@JasonColeyNZ
Copy link

My implementation of supabase looks like this

function getSupabaseServerClient(
	cookieHeader: string | undefined,
	env: APP_ENV,
	params = { admin: false }
) {
	const headers = new Headers()
	// const cookieHeader = requestHeaders.get("Cookie") || undefined
	invariantResponse(env.SUPABASE_URL, SUPABASE_URL_NOT_PROVIDED)
	invariantResponse(env.SUPABASE_ANON_KEY, SUPABASE_ANON_KEY_NOT_PROVIDED)
	if (params.admin) {
		const serviceRoleKey = env.SUPABASE_SERVICE_ROLE_KEY
		invariantResponse(serviceRoleKey, SUPABASE_SERVICE_ROLE_KEY_NOT_PROVIDED)
		return {
			client: getSBClient(
				headers,
				env.SUPABASE_URL,
				serviceRoleKey,
				cookieHeader
			),
			headers,
		}
	}

	return {
		client: getSBClient(
			headers,
			env.SUPABASE_URL,
			env.SUPABASE_ANON_KEY,
			cookieHeader
		),
		headers,
	}
}
export function getSBClient(
	headers: Headers,
	SUPABASE_URL: string,
	SUPABASE_ANON_KEY: string,
	cookieHeader: string | undefined
) {
	return createServerClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
		cookies: {
			getAll() {
				return parseCookieHeader(cookieHeader ?? "")
			},
			setAll(cookiesToSet) {
				for (const cookie of cookiesToSet) {
					headers.append(
						"set-cookie",
						serializeCookieHeader(cookie.name, cookie.value, cookie.options)
					)
				}
			},
		},
	})
}

then in my actions/loaders etc I can use...

const { client: supabaseClient, headers } = getSupabaseServerClient(
		cookieHeader,
		context.env,
		{
			admin: true,
		}
	)

then I return these headers into the response. This also allows me to grab an admin sb client if needed.

@Kylar13
Copy link
Author

Kylar13 commented Jan 28, 2025

Have you tried using redirect() with the same headers used to initialize the client? That's what Supabase Auth needs to validate the auth request on the OAuth flow, and that's what's causing the issue right now.

On email flow and in other places where the client is used, there's no issue.

@Kylar13
Copy link
Author

Kylar13 commented Jan 28, 2025

Got the bug-report thing to work, posted a pull request at #12876

@JasonColeyNZ
Copy link

JasonColeyNZ commented Jan 28, 2025

OK sorry the process above is what I use for the token auth.

For Google I have been using ReactOAuth2|Google and the One tap signin process.
https://www.npmjs.com/package/@react-oauth/google

on the form I have

	const handleOneTapSignIn = async (credentialResponse: CredentialResponse) => {
		if (!credentialResponse.credential) return
		const supabase = createBrowserClient(
			env.SUPABASE_URL || "",
			env.SUPABASE_ANON_KEY || ""
		)
		const { data } = await supabase.auth.signInWithIdToken({
			provider: "google",
			token: credentialResponse.credential,
		})
		if (data) {
			navigate("/dashboard")
		}
	}

...

<GoogleLogin
								onSuccess={handleOneTapSignIn}
								onError={() => {
									
								}}
								use_fedcm_for_prompt
							/>

@JasonColeyNZ
Copy link

JasonColeyNZ commented Jan 28, 2025

When i sign in using OAuth I use this function on my form...

const handleSignInWithGithub = async () => {
		const supabase = createBrowserClient(
			env.SUPABASE_URL || "",
			env.SUPABASE_ANON_KEY || ""
		)
		// console.log("env.GITHUB_OAUTH_REDIRECT_URI", env.OAUTH_SIGNUP_REDIRECT_URI)
		await supabase.auth.signInWithOAuth({
			provider: "github",
			options: {
				redirectTo: `${env.OAUTH_SIGNUP_REDIRECT_URI}`,
			},
		})
	}

This is what my callback looks like..

import chalk from "chalk"
import { data, redirect } from "react-router"
import { emsLogger } from "~/server"
import getSupabaseServerClient from "~/services/supabase"
import { safeRedirect } from "~/utils.server"
import { createToastResponse } from "~/utils/toast.server"
import type { Route } from "./+types/oauthsignupcallback"

const debug = true

export const loader = async ({ request, context }: Route.LoaderArgs) => {
	if (debug)
		emsLogger(chalk.blueBright("auth.oauthSignUpCallback.loader"), request.url)
	const requestUrl = new URL(request.url)
	const response = new Response()

	const code = requestUrl.searchParams.get("code") || "/"

	if (code) {
		const cookieHeader = request.headers.get("Cookie") || undefined
		const { client: supabase, headers } = getSupabaseServerClient(
			cookieHeader,
			context.env,
			{
				admin: true,
			}
		)
		const { data, error } = await supabase.auth.exchangeCodeForSession(code)
		if (debug)
			emsLogger(chalk.green("exchangeCodeForSession.data"), data !== null)
		if (error) emsLogger(chalk.red("exchangeCodeForSession.error"), error)
		if (!error) {
			const {
				data: { user },
			} = await supabase.auth.getUser(data.session.access_token)

			if (debug)
				emsLogger(chalk.yellow("getUser"), user !== null, user ? user.id : null)

			const redirectTo = safeRedirect("/dashboard")

			return redirect(redirectTo, { headers })
		}
		await createToastResponse(context, "Error", error.message, "destructive")
		throw redirect("/signin")
	}
	return data({}, { headers: response.headers })
}

@Kylar13
Copy link
Author

Kylar13 commented Jan 29, 2025

@JasonColeyNZ Yeah, you're doing OAuth from the client library, I wanted to set it up to all be server-side for this project, and ran into this bug
I'm surprised your return redirect(redirectTo, { headers }) doesn't crash on the callback loader, maybe it's only an issue on actions or the biggest skill issue on my side, not sure yet.

Thanks for the help anyway!

@msevestre
Copy link

@Kylar13 We are having the same issue after migrating to react-router from remix6
Error: Unable to decode turbo-stream response

I managed to boil it down to an action returning a response with a header set exactly as you. Did you manage to solve your issue?

@Kylar13
Copy link
Author

Kylar13 commented Mar 25, 2025

@Kylar13 We are having the same issue after migrating to react-router from remix6
Error: Unable to decode turbo-stream response

I managed to boil it down to an action returning a response with a header set exactly as you. Did you manage to solve your issue?

Hey!! No I did not, filed a bug report test showcasing the bug and even tried to dig into the code myself but got nowhere.
Also asked around in discord but got no traction.

Unfortunately, since I encountered this at the beginning of an experiment, I just ended up moving to using tanstack-start for it

@moneyDev1111
Copy link

moneyDev1111 commented Mar 31, 2025

I have the same issue

just fixed it

so I remove all the headers from the request and took only needed ones which is cookies, and since I have an array of them I did it like this... or the copilot did like after 1000000000001 trials.........

export const action: ActionFunction = async ({ request }) => {
	const formData = await request.formData()

	const response = await client.post('/login', { ...Object.fromEntries(formData) })

	const headers = new Headers()
	const cookies = response.headers['set-cookie']

	if (Array.isArray(cookies)) {
		cookies.forEach((cookie) => headers.append('set-cookie', cookie))
	} else if (cookies) {
		headers.append('set-cookie', cookies)
	}

	return redirect('/pictures', {
		headers,
	})
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants