GraphQL Mutation 在 Playground 中有效,但在客户端代码中无效

GraphQL Mutation works in Playground but not in Client Code

在这里看到类似的问题,但无法弄清楚我做错了什么。我有这个突变刷新 refreshToken 和访问 token:

type Auth {
    user: User!
    token: String!
    refreshToken: String!
}

type Mutation {
    refreshTokens: Auth!
}

解析器:

Mutation: {
    refreshTokens: async (root, args, { req }, info) => {
      const result = await issueNewToken(req);
      return result;
    },
}

授权助手:

issueNewToken: async (req) => {
    try {
      const token = req.headers.rt;
      if (token) {
        const decoded = await jwt.verify(token, jwtRefreshTokenSecret);
        let user = await User.findById(decoded.id);

        if (!user) {
          throw new AuthenticationError("No user found.");
        }
        let tokens = await authFunctions.issueToken(user);
        return { ...tokens, user };
      }
    } catch (err) {
      throw new AuthenticationError("Invalid Refresh Token.");
    }
  },

然后需要刷新令牌的函数如果过期:

const refreshTokens = async () => {
  const {
    data: {
      refreshTokens: {
        user: { _id: id, address: address },
        token: token,
        refreshToken: refreshToken,
      },
    },
  } = await renewTokenApiClient.mutate({
    mutation: REFRESH_TOKENS,
  });

  localStorage.setItem("refreshtoken", refreshToken);
  localStorage.setItem("token", token);
};

和突变:

export const REFRESH_TOKENS = gql`
  mutation refreshTokens {
    refreshTokens {
      user {
        _id
        address
      }
      token
      refreshToken
    }
  }
`;

现在,这在操场上有效。当我在 http header 中传递刷新令牌时,我得到了更新的令牌和 refreshToken。但是refreshTokens上面的函数returns

"Cannot return null for non-nullable field Mutation.refreshTokens."

我在这里错过了什么?

更新:

问题确实是操作不正确 headers。在 Whosebug 上的一些文档和其他问题之后,我形成了以下函数,但是,该操作仍然没有得到 headers,即使我似乎在刷新令牌后将它们设置在 header 中(哪个工作正常)。

import {
  ApolloClient,
  createHttpLink,
  fromPromise,
  from,
} from "@apollo/client";
import { onError } from "@apollo/client/link/error";
import { REFRESH_TOKENS } from "../mutations/User";
import { setContext } from "@apollo/client/link/context";

import cache from "../cache";

let isRefreshing = false;
let pendingRequests = [];

const setIsRefreshing = (value) => {
  isRefreshing = value;
};

const addPendingRequest = (pendingRequest) => {
  pendingRequests.push(pendingRequest);
};

const headerLink = setContext((_, { headers }) => {
  const token = localStorage.getItem("refreshtoken");
  return {
    headers: {
      ...headers,
      rt: token ? token : "",
    },
  };
});

const renewTokenApiClient = new ApolloClient({
  link: from([headerLink, createHttpLink({ uri: "/graphql" })]),
  cache,
  credentials: "include",
});

const resolvePendingRequests = () => {
  pendingRequests.map((callback) => {
    console.log(callback);
    callback();
  });
  pendingRequests = [];
};

const refreshTokens = async () => {
  const {
    data: {
      refreshTokens: {
        user: { _id: id, address: address },
        token: token,
        refreshToken: refreshToken,
      },
    },
  } = await renewTokenApiClient.mutate({
    mutation: REFRESH_TOKENS,
  });

  localStorage.setItem("refreshtoken", refreshToken);
  localStorage.setItem("token", token);
};

const errorLink = onError(
  ({ graphQLErrors, networkError, operation, forward }) => {
    if (graphQLErrors) {
      for (const err of graphQLErrors) {
        switch (err?.message) {
          case "jwt expired":
            const oldHeaders = operation.getContext().headers;

            if (!isRefreshing) {
              setIsRefreshing(true);
              const headers = {
                ...operation.getContext().headers,
              };
              return fromPromise(
                refreshTokens()
                  .then((refreshTokensResult) => {
                    let headers = {
                      //readd old headers
                      oldHeaders,
                      //switch out old access token for new one
                      authorization: `Bearer ${refreshTokensResult.token}`,
                      rt: refreshTokensResult.refreshToken,
                    };

                    operation.setContext({
                      headers,
                    });

                    console.log(operation, "ops");
                  })
                  .catch(() => {
                    resolvePendingRequests();
                    setIsRefreshing(false);

                    localStorage.clear();

                    return forward(operation);
                  })
              ).flatMap(() => {
                resolvePendingRequests();
                setIsRefreshing(false);

                console.log(operation);

                return forward(operation);
              });
            } else {
              return fromPromise(
                new Promise((resolve) => {
                  addPendingRequest(() => resolve());
                })
              ).flatMap(() => {
                return forward(operation);
              });
            }
        }
      }
    } else if (networkError) console.log(`[Network error]: ${networkError}`);
  }
);

export default errorLink;

UPD2: 找到解决方案并发布答案。

查看 issueNewToken 的代码,如果 header 中不存在令牌,则有一条路径将 return undefined。目前尚不清楚您是如何创建 renewTokenApiClient 的,但是需要在某处指定 header 以随请求一起发送,它不像自动发送的 cookie。

我建议检查网络选项卡以查看请求中是否存在 header。如果不是,则根据客户端的使用情况,可以通过多种方式注入它。对于 apollo 客户端,你需要创建 a link 来注入它。

import { setContext } from '@apollo/client/link/context';

const authenticationLink = setContext((_, { headers }) => {
  const token = localStorage.getItem('token');
  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : "",
    }
  }
});

完成了这个,效果很好。 refreshTokens() 在 localStorage 中设置新令牌。

const errorLink = onError(
  ({ graphQLErrors, networkError, operation, forward }) => {
    if (graphQLErrors) {
      for (const err of graphQLErrors) {
        switch (err?.message) {
          case "jwt expired":
            // Let's refresh token through async request
            return new Observable((observer) => {
              refreshTokens()
                .then(() => {
                  operation.setContext(({ headers = {} }) => ({
                    headers: {
                      ...headers,
                    },
                  }));
                })
                .then(() => {
                  const subscriber = {
                    next: observer.next.bind(observer),
                    error: observer.error.bind(observer),
                    complete: observer.complete.bind(observer),
                  };

                  // Retry last failed request
                  forward(operation).subscribe(subscriber);
                })
                .catch((error) => {
                  // No refresh or client token available, we force user to login
                  localStorage.clear();
                  isLoggedInVar(false);
                  observer.error(error);
                });
            });
        }
      }
    } else if (networkError) console.log(`[Network error]: ${networkError}`);
  }
);