Skip to content
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

Ably: Auth.requestToken(): token request signing call returned error; err = Error: input must not start with a slash when using prefixUrl #1562

Open
TranquilMarmot opened this issue Jan 4, 2024 · 4 comments
Labels
enhancement New feature or improved functionality.

Comments

@TranquilMarmot
Copy link

TranquilMarmot commented Jan 4, 2024

I'm trying to add Ably to a Next.js 14 project.

On the client-side, I'm creating the Ably client like this:

import Ably from "ably/promises";

// ...

export const createAblyRealtimeClient = (authParams?: Record<string, string>) =>
  new Ably.Realtime({
    authUrl: "/api/ably-auth",
    authMethod: "POST",
    authParams,
  });

const client = ablyRealtimeClient();

I'm using the AblyProvider like so:

import { AblyProvider } from "ably/react";
import { Session } from "next-auth";
import { FunctionComponent, PropsWithChildren } from "react";

// method defined above
import { createAblyRealtimeClient } from "@/lib/ably";

interface AblyProviderProps {
  session: Session | null;
}

const client = createAblyRealtimeClient();

export const MyAblyProvider: FunctionComponent<
  PropsWithChildren<AblyProviderProps>
> = ({ session, children }) => {
  if (!session) {
    return children;
  }

  return (
    <AblyProvider client={client} id="some-id">
      {children}
    </AblyProvider>
  );
};

And then on the server, I have a file at (...)/api/ably-auth/route.ts that's doing this:

import Ably from "ably/promises";

// ...

const ably = new Ably.Rest({ key: process.env.ABLY_SECRET_KEY });

export async function POST(request: Request) {
  const session = await getSession();

  if (!session) {
    return NextResponse.json({ error: "Not logged in" }, { status: 401 });
  }

  const tokenRequest = await ably.auth.createTokenRequest({
    clientId: session.user.id,
  });
  
  return new Response(JSON.stringify(tokenRequest), { status: 200 });
}

This seems to be working just fine. The server responds with a tokenRequest.

But, in the server logs, every time that createTokenRequest is called, this is printed out:

Ably: Auth.requestToken(): token request signing call returned error; err = Error: `input` must not start with a slash when using `prefixUrl`- - -I see this mentioned in this issue:https://github.com/ably-labs/react-hooks/issues/8#issuecomment-1697410212

But this doesn't make any sense to me:

your NextJS application is creating an Ably client on the server side (ie in a NodeJS process) but really the Ably client should only be created on the client side

If I'm doing token authentication, wouldn't I ONLY ever want to create a client on the server? Otherwise, I'd be passing the API key to the client which seems like a Bad Idea.

Maybe I'm missing something here? Are we not supposed to use the JS client on the server?

These docs seem to suggest that this is what we're supposed to do:
https://ably.com/docs/auth/token?lang=javascript#token-request

Using an Ably SDK, a TokenRequest is generated from your server and returned to the client-side SDK instance.

┆Issue is synchronized with this Jira Story by Unito

Copy link

sync-by-unito bot commented Jan 4, 2024

➤ Automation for Jira commented:

The link to the corresponding Jira issue is https://ably.atlassian.net/browse/SDK-4014

@owenpearson
Copy link
Member

Hey @TranquilMarmot, thanks for reaching out!

If I'm doing token authentication, wouldn't I ONLY ever want to create a client on the server? Otherwise, I'd be passing the API key to the client which seems like a Bad Idea.

I think there's a bit of ambiguity here, typically we refer to the realtime SDK instance as the client, but if you're also using a REST SDK instance to generate signed token requests then this is also a client. In your case, only the REST client has access to your Ably API key, and you're absolutely right that this client should only be created on the server side.

On the other hand, the realtime client creates a stateful connection in order to subscribe to realtime updates so if you want your client web application to receive realtime messages over Ably this realtime client must be created on the client side. If you are using a token auth method (such as authUrl) then this client will authenticate with short-lived tokens so your API key won't be exposed.

The "input must not start with a slash" error itself comes from got which is the http client used by ably-js when it runs in Node.js, and it will occur if you use an authUrl beginning with a / character in Node.js (the reason this is an error is simply that there's no sensible way to resolve a path beginning with a / into a full URL in Node.js). The error will never occur for a client which is created in a web browser context or a client which is using an API key to authenticate, so in your case it must be coming from the realtime client being created in Node.js.

In my experience, NextJS is quite eager to pre-render JSX into HTML on the server side for the sake of optimisation so I'm fairly certain what's happening here is that your NextJS server is running the createAblyRealtimeClient function on the server side to try and pre-render the MyAblyProvider component. This pre-rendering isn't necessary for the application to work so you might find that, even though you see this error, the application still works fine anyway?

In order to get rid of the error I would recommend adding a 'use client' directive to the MyAblyProvider component and if that doesn't work then check the NextJS docs to see other techniques you can use to ensure that this code doesn't run on the server side.

@TranquilMarmot
Copy link
Author

TranquilMarmot commented Jan 5, 2024

Thank you for the detailed response!

I think you're right that it's because the server is rendering the new Ably.Realtime({ authUrl: "/api/ably-auth" });. You would think that adding "use client"; to the MyAblyProvider would fix it, but it doesn't.

The comment that I linked above (ably-labs/react-hooks#8 (comment)) makes a lot more sense now 😅

It's worth mentioning also that, while the name 'Client Components' (ie components with the 'use client' directive) implies that these components will only be rendered on the client side, NextJS may still run the component code on the server in order to pre-render HTML so you may still need to use dynamic loading in order to avoid Ably clients being created on the server side.

Just adding "use client"; won't fix the issue, but it looks like wrapping the component in next/dynamic fixes the issue...

"use client";

import dynamic from "next/dynamic";

const DynamicAblyProvider = dynamic(
  () => import("./my-ably-provider"),
  {
    ssr: false, // this ensures that server side rendering is never used for this component
  },
);

export default DynamicAblyProvider;

This is a bit of a bummer!

I wonder if there could be a check in the JS client to detect that it's running outside of a browser context and then skip doing the auth call with got? You can check with typeof window === 'undefined'. It would make it a lot easier to use Ably with Next.js, which I think will be a very common use case 😄

I wonder if this would be an issue with server rendering in the example in the Ably tutorials; https://ably.com/tutorials/how-to-ably-react-hooks#tutorial-step-3
It would be great to at least have this behavior documented somewhere in the Ably docs.

@TranquilMarmot
Copy link
Author

TranquilMarmot commented Jan 5, 2024

Another workaround that's a little less crazy... you can wrap creating the client inside of a useEffect to only create it on the client side.

const createAblyRealtimeClient = (authParams?: Record<string, string>) =>
  new Realtime({
    authUrl: AUTH_URL,
    authMethod: "POST",
    authParams: authParams ?? {},
    useTokenAuth: true,
  });

export const useAblyRealtimeClient = (authParams?: AblyAuthParams) => {
  const clientRef = useRef<Realtime | null>(null);

  useEffect(() => {
    if (typeof window !== "undefined") {
      clientRef.current?.close();
      clientRef.current = createAblyRealtimeClient(authParams);
    }

    return () => {
      clientRef.current?.close();
      clientRef.current = null;
    };
  }, []);

  return clientRef.current;
};

When using it, you can manually do a fallback...

export const MyAblyProvider: FunctionComponent<
  PropsWithChildren<AblyProviderProps>
> = ({ session, children }) => {
  const client = useAblyRealtimeClient();

  // this means we're rendering on the server
  if (!client) {
     return <div>Loading...</div>
  }

  if (!session) {
    return children;
  }

  return (
    <AblyProvider client={client} id="some-id">
      {children}
    </AblyProvider>
  );
};

@VeskeR VeskeR added the enhancement New feature or improved functionality. label Apr 19, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or improved functionality.
Development

No branches or pull requests

3 participants