apollographql / apollo-client-nextjs

Apollo Client support for the Next.js App Router
https://www.npmjs.com/package/@apollo/experimental-nextjs-app-support
MIT License
358 stars 25 forks source link

Access headers of response in client side #111

Closed Stevemoretz closed 7 months ago

Stevemoretz commented 7 months ago

The API that I use sends some headers in response which are as useful as data that it sends, I can grab the data in server and client thanks to this library, for headers before all these SSR stuff I had axios interceptors and handled it in the client side, right now I just want to grab the headers somehow from apollo response, or somehow send those intercepted headers from server side to client side (I need some guidance in how to make it possible like you did with your data from server to client)

phryneas commented 7 months ago

I'm sorry, but I'm a bit confused what you're actually trying to do here. Could you be a bit more specific and maybe add code examples of what you did before?

Stevemoretz commented 7 months ago

Sure thank you for responding and sorry for being vague. Here's a simple example:

axios.interceptors.response.use(
    (response) => {
        const user = response.request.getResponseHeader("User");
        if (user) {
            localStorage.setItem("user", JSON.parse(user)));
        }
        return response;
    }
);

In the above example we are listening to responses and if any of them have a header named "User" we want to store it in localStorage so we always have the latest user, now this method works pretty well when not using SSR and is a pretty simple yet powerful method of having fresh data, but when using SSR localStorage isn't obviously accessible on the server side that those interceptors are running, therefore we can't use them in this way anymore.

While this method keeps working for client side requests such as a useMutation, it could be great if there was a way to send some of that data from server to client to use, in this example the headers would be great to be sent to the client, for implementation a simple callback on the useSuspenseQuery options can do the trick or maybe returning headers alongside data in useSuspenseQuery function.

But if there was a simple way of sending anything from server to client in the first render, that could make it flexible for lots of other use cases, but the concept itself probably doesn't have anything to do with this library :) only the header part does.

phryneas commented 7 months ago

Phew, there's no easy way to do that.

You could use a custom link to look at the result and set result.context to some value you want to transport (that link will also have access to operationContext.response).

That would bring it to the other side, but you have no good way of accessing it there. You'd have to wrap NextSSRApolloClient.write, e.g. by implementing your own version:

class MyWrappingNextSSRInMemoryCache extends NextSSRInMemoryCache {
  write(result) {
    console.log(result)
    // add your logic reading `result.context` here
    super.write(result)
  }
}
Stevemoretz commented 7 months ago

class MyWrappingNextSSRApolloClient extends NextSSRApolloClient { write(result) { console.log(result) // add your logic reading result.context here super.write(result) } }

Thank you so much for showing the way!

I can't find the "write" method in that class (typescript and IDE error, also no log on the console from my console.log that I put in there), did you mean maybe writeQuery or writeFragment?

export class MyWrappingNextSSRApolloClient<T> extends NextSSRApolloClient<T> {
    write(result) {
        console.log(result);
        super.write(result);
    }
}
phryneas commented 7 months ago

Oh I'm sorry, you have to do that with NextSSRInMemoryCache - my fault!

Stevemoretz commented 7 months ago

Oh I'm sorry, you have to do that with NextSSRInMemoryCache - my fault!

Thanks again! Context doesn't seem to go through though maybe I did something wrong.

new ApolloLink((operation, forward) => {
  return forward(operation).map((response) => {
      const context = operation.getContext();
      context["_headers_"] = {
          hello: "world",
      };
      operation.setContext(context);
      return response;
  });
}),
export class MyNextSSRInMemoryCache extends NextSSRInMemoryCache {
    write(options) {
        console.log(typeof window === "undefined", options.context, options);
        return super.write(options);
    }
}

The log output shows that it only runs on the server and context wasn't actually injected:

true undefined {
  query: {
    kind: 'Document',
    definitions: [ [Object] ],
    loc: Location { start: 0, end: 493, source: [Source] }
  },
  variables: { id: 85 },
  overwrite: false,
  dataId: 'ROOT_QUERY',
  result: {
    box: {
      id: 85,
      title: 'Laboriosam eos est doloribus.',
      description: '<p>Eum officia in possimus dolor hic praesentium voluptate. Perspiciatis expedita fugit commodi qui. Ullam incidunt et delectus laboriosam nostrum perspiciatis. Quidem earum itaque rerum reiciendis animi sed.</p>',
      photo_hash: 'L3G[[W+^4Y4.04t0?[Dk00P701^l',
      stream_date: null,
      shipped: false,
      slots: [Array],
      __typename: 'Box'
    }
  }
}
phryneas commented 7 months ago

There is a property context on response that should go through - it's not the normal context you'd set with setContext.

Tbh., I've never seen this used anywhere before, but it should do what you need here.

new ApolloLink((operation, forward) => {
  return forward(operation).map((response) => {
    const context = operation.getContext();
    return {
      ...response,
      context: { ...response.context, hello: "world" },
    };
  });
});
Stevemoretz commented 7 months ago

There is a property context on response that should go through - it's not the normal context you'd set with setContext.

Tbh., I've never seen this used anywhere before, but it should do what you need here.

new ApolloLink((operation, forward) => {
  return forward(operation).map((response) => {
    const context = operation.getContext();
    return {
      ...response,
      context: { ...response.context, hello: "world" },
    };
  });
});

Thanks again! Github didn't sent me a notification I would have checked this instantly otherwise, so I did exactly as you mentioned but still I couldn't get inside the NextSSRInMemoryCache class:

                  new ApolloLink((operation, forward) => {
                      return forward(operation).map((response) => {
                          console.log("sending", {
                              ...(response?.["context"] || {}),
                              hello: "world",
                          });
                          return {
                              ...response,
                              context: {
                                  ...(response?.["context"] || {}),
                                  hello: "world",
                              },
                          };
                      });
                  }),

and:

export class MyNextSSRInMemoryCache extends NextSSRInMemoryCache {
    write(options) {
        // console.log(typeof window === "undefined", options.context, options);
        console.log(options);
        return super.write(options);
    }
}

My logs are:

sending { hello: 'world' }

and second log on both server and browser is:

{
  query: {
    kind: 'Document',
    definitions: [ [Object] ],
    loc: Location { start: 0, end: 493, source: [Source] }
  },
  variables: { id: 85 },
  overwrite: false,
  dataId: 'ROOT_QUERY',
  result: {
    box: {
      id: 85,
      title: 'Laboriosam eos est doloribus.',
      description: '<p>Eum officia in possimus dolor hic praesentium voluptate. Perspiciatis expedita fugit commodi qui. Ullam incidunt et delectus laboriosam nostrum perspiciatis. Quidem earum itaque rerum reiciendis animi sed.</p>',
      photo_hash: 'L3G[[W+^4Y4.04t0?[Dk00P701^l',
      stream_date: null,
      shipped: false,
      slots: [Array],
      __typename: 'Box'
    }
  }
}
phryneas commented 7 months ago

Phew, I had hoped that would work.

I feat in that case, you'll have to fiddle it into result.data on the server and then remove it on the client before calling return super.write(options);

Not really clean, I know, but probably your only possibility here :/

Stevemoretz commented 7 months ago

Phew, I had hoped that would work.

I feat in that case, you'll have to fiddle it into result.data on the server and then remove it on the client before calling return super.write(options);

Not really clean, I know, but probably your only possibility here :/

Incredible that actually works :) I'm just happy it was even possible, here's a final example I'm not sure if it's actually a good idea to do this, but I'm trying to get cookies to come across in this example.

So here's the final link that is only set on the server side and not the client side.

                  new ApolloLink((operation, forward) => {
                      return forward(operation).map((response) => {
                          const cookie = operation
                              .getContext()
                              .response.headers.get("set-cookie");
                          return {
                              ...response,
                              data: {
                                  ...response.data,
                                  _context_: {
                                      cookie: cookie,
                                  },
                              },
                          };
                      });
                  }),

Here's our custom cache class:

import {NextSSRInMemoryCache} from "@apollo/experimental-nextjs-app-support/ssr";

export class MyNextSSRInMemoryCache extends NextSSRInMemoryCache {
    write(options) {
        if (typeof window !== "undefined") {
            const context = options.result?._context_;
            if (context) {
                if (context.cookie) {
                    document.cookie = context.cookie;
                    console.log("set cookie to ", document.cookie);
                }
                delete options.result?._context_;
            }
        }
        return super.write(options);
    }
}

I get XSRF on the cookie and thought maybe it's a good idea to do this, but I'm not sure how well it works (not tested it very well yet), if it's a good idea maybe the cookie part should be done officially.


Probably not a good idea anyway, since it will send http only and secure cookies to client directly this could be a better idea if nextjs did support setting cookies from there but it doesn't, only supports route handlers and server actions at the moment...

phryneas commented 7 months ago

Let's just say: This is a very cool experiment, but I really hope that Next will support something like this out of the box 🤣

Stevemoretz commented 7 months ago

Let's just say: This is a very cool experiment, but I really hope that Next will support something like this out of the box 🤣

Lol it is indeed pretty cool, the header part is good enough for now honestly! :D I really appreciate your guidance, I couldn't ever figure this solution without your help. Feel free to close this, I didn't close it since maybe you guys want to leave it here as a reminder if next adds support.

phryneas commented 7 months ago

Feel free to close this, I didn't close it since maybe you guys want to leave it here as a reminder if next adds support.

Yeah... general problem with a lot of the issues here... should we close them or keep them open for visibility? 😅 I'm gonna close this one, and I hope people find it using the search function if they need this :)