apollographql / apollo-client

:rocket:  A fully-featured, production ready caching GraphQL client for every UI framework and GraphQL server.
https://apollographql.com/client
MIT License
19.4k stars 2.66k forks source link

useSubscription throwing error 400 bad request in react native #9821

Open FaisalSeraj-techverx opened 2 years ago

FaisalSeraj-techverx commented 2 years ago

I am working on a project that has a web portal and a react native application. at some stage, I need to get subscription data via useSubscription hook. I used apollo client docs to configure the web socket with Graphql-Ws, that works fine in react web portal but when it comes to react-native it throws blind 400 bad request and it is hard to debug the actual network call. my package.json looks like this: "dependencies": { "@apollo/client": "^3.6.8", "@apollo/react-hooks": "^4.0.0", "@freakycoder/react-native-bounceable": "^0.2.5", "@freakycoder/react-native-custom-text": "0.1.2", "@freakycoder/react-native-helpers": "^1.0.2", "@react-native-async-storage/async-storage": "^1.17.6", "@react-native-community/clipboard": "^1.5.1", "@react-native-community/masked-view": "^0.1.11", "@react-native-picker/picker": "^2.4.1", "@react-navigation/bottom-tabs": "^6.0.9", "@react-navigation/native": "^6.0.6", "@react-navigation/stack": "^6.0.11", "@twotalltotems/react-native-otp-input": "1.3.7", "@types/graphql": "^14.5.0", "apollo": "^2.33.9", "apollo-link": "^1.2.14", "apollo-link-error": "^1.1.13", "apollo3-cache-persist": "^0.14.0", "axios": "^0.24.0", "events": "^3.3.0", "formik": "^2.2.9", "graphql": "^16.5.0", "graphql-ws": "^5.9.0", "jwt-decode": "^3.1.2", "moment": "^2.29.3", "react": "17.0.2", "react-native": "0.66.4", "react-native-animatable": "^1.3.3", "react-native-auth0": "^2.13.1", "react-native-calendars": "^1.1284.0", "react-native-document-picker": "^8.1.0", "react-native-dropdown-picker": "^5.4.2", "react-native-dynamic-vector-icons": "^1.1.6", "react-native-file-viewer": "^2.1.5", "react-native-fs": "^2.20.0", "react-native-gesture-handler": "^2.1.1", "react-native-localization": "^2.1.7", "react-native-paper": "^4.12.1", "react-native-paper-dropdown": "^1.0.7", "react-native-phone-number-input": "^2.1.0", "react-native-reanimated": "^2.3.1", "react-native-rename": "^2.9.0", "react-native-safe-area-context": "^3.3.2", "react-native-screens": "^3.10.1", "react-native-splash-screen": "^3.3.0", "react-native-svg": "^12.3.0", "react-native-svg-transformer": "^1.0.0", "react-native-vector-icons": "^9.1.0", "react-navigation-helpers": "^2.0.0", "rn-fetch-blob": "^0.12.0", "yup": "^0.32.11"

**Error:** 

image

note!!! backend is hasura

@bruce @probablycorey @rgrove @ivank let me know if any other info is needed 
FaisalSeraj-techverx commented 2 years ago

I have figured it out by downgrading the apollo/client from 3.6.8 to 3.4.16 and graphql-ws to 5.5.5.

jpvajda commented 2 years ago

@FaisalSeraj-techverx thanks for letting us know, though we'd hope you wouldn't have to downgrade to do this, so I'll leave this open to investigate.

FaisalSeraj-techverx commented 2 years ago

@jpvajda since the average response time on this repo is one month it was hard to find out a solution other than that so definitely needed to downgrade these packages otherwise project wouldn't have been delivered. Anyway I appreciate you are looking into the problem and hopefully will propose a solution soon

jpvajda commented 2 years ago

@FaisalSeraj-techverx totally understand! As a note, we plan to increase our response time in this repo as Apollo grow's the OSS team that supports Apollo Client, so in the near future I hope you will see a quicker overall response to questions and issues.

FaisalSeraj-techverx commented 2 years ago

@jpvajda Thanks, Really appreciate the way you handle things

wildermuthn commented 2 years ago

Am I understanding correctly that subscriptions do not work in the latest versions of Apollo Client running in React Native?

mirko-console-dir commented 9 months ago

Like this is working, hopefully I can help someone Server Side: index.js

import { ApolloServer } from '@apollo/server';
import {createServer} from 'http'
import authenticateUser from './middleware/auth/auth.js'
import express from 'express';
import cors from "cors"
import { expressMiddleware } from '@apollo/server/express4';
import { PORT, IN_PROD, DB } from './config/index.js'
import mongoose from 'mongoose';
import { typeDefs ,resolvers }from './graphql/index.js';
import { refreshTokenHandler } from './middleware/auth/refreshTokenHandler.js';
import cookieParser from 'cookie-parser';
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
import bodyParser from 'body-parser';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import {WebSocketServer} from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { makeExecutableSchema } from '@graphql-tools/schema';

/* To connect the app with MongoDB, we need mongoose */
async function startServer() {

  try {
    // Connect to MongoDB
    await mongoose.connect(DB, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
      dbName: 'todo_app',
    });
    console.log('Database connected');

    const app = express();

   app.use(cors({
      origin: ['http://localhost:4000' ], // or whatever your client's origin is
      methods: "GET,HEAD,PUT,PATCH,POST,DELETE",
      credentials: true,
      optionsSuccessStatus: 204
    }));    

    app.use(bodyParser.json({ limit: '50mb', extended: true }));
    app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
    app.use(cookieParser());

    // Create an Apollo Server instance
    /*  Subscription Server Setup */
    const httpServer = createServer(app);
    const schema = makeExecutableSchema( { typeDefs, resolvers } );

    const wsServer = new WebSocketServer({
      server: httpServer,
      path: '/graphql'
    })

    const serverCleanup = useServer({ schema }, wsServer);

    const server = new ApolloServer({
      /* Subscription Server Setup */
      schema,
      typeDefs,
      resolvers,
      plugins: [
        // Proper shutdown for the HTTP server.
        ApolloServerPluginDrainHttpServer({ httpServer }),
        // Proper shutdown for the WebSocket server.
        {
          async serverWillStart() {
            return {
              async drainServer() {
                await serverCleanup.dispose();
              },
            };
          },
        },
      ],
      /* END Subscription Server Setup */
      /* END Subscription Server Setup */
      playground: true,
      introspection: true,  //  might be disabled in production
      useNewUrlParser: true, 
      useUnifiedTopology: true, 
      uploads: true,
    });

    await server.start();

    app.use(express.json())
    app.use(express.urlencoded({extended: true}))

    app.post('/refreshtoken', refreshTokenHandler); // Endpoint for refreshing tokens

    app.use(graphqlUploadExpress());

    app.use("/graphql", expressMiddleware(server, {
      context: async ({ req, res }) => {
            // List of operations that do not require authentication as from client sent
            const unauthenticatedOperations = ['CreateUser', 'LoginUser'];
            // Get the GraphQL operation name from the request
            const operationName = req.body.operationName;
            // Check if the operation requires authentication
            const requiresAuthentication = unauthenticatedOperations.includes(operationName);
            // Authenticate the user only if required
            const user = !requiresAuthentication ? authenticateUser(req) : null;

            console.log('is arrived');

            const other = 'otherServiceMiddle(req)' // this just for example to show we can pass more stuff in the context
            return {user , other, req , res}
      },
    })) 

    /* app.listen(PORT, () => {
      console.log(`Express is running at http://localhost:${PORT}`);
      console.log(`Graphql is running at http://localhost:${PORT}/graphql`);
    }); */
    httpServer.listen(PORT, () => {
      console.log(`Express is running at http://localhost:${PORT}`);
      console.log(`GraphQL is running at http://localhost:${PORT}/graphql`);
      console.log(`WebSocket server is running at ws://localhost:${PORT}/graphql`);
    });

  } catch (error) {
    console.error(error);
  }
}

startServer();

resolver.js

 Mutation: {
        addCommentTodo: async (_,{ todoId, input },contextValue) => {
            const { commentText } = input;

            if (!contextValue.user) {
                throw new Error("Not authenticated");
            };
            try {
                const user = await User.findById(contextValue.user._id);
                const author = {id: user._id, fullname:user.fullname, avatar: user.avatar};

                const newComment = {    
                    commentText: commentText, 
                    author: author,
                }

                /* PING THE SUBSCRIPTION 'triggername', {name of the subscription and type of commentCreated } */
                    pubsub.publish('COMMENT_CREATED', {
                        commentCreated: newComment
                    });

                const todo = await Todo.findByIdAndUpdate(
                    todoId,
                    {
                        $push: { comments: newComment },
                    },
                    { new: true }
                );
                if (!todo) throw new Error("Todo not found");
                const storedComment = todo.comments[todo.comments.length - 1];

                return storedComment;
            }catch (err) {
                throw new Error("Error add comment ", err);
            }
        },
    },
    Subscription: {
        commentCreated: {
    /* when someone subscribe we use asyncIterator is going to wait an event to be trigged and give to the user a response */
            subscribe: () => {
                console.log('====================================');
                console.log('trigged sub');
                return pubsub.asyncIterator(['COMMENT_CREATED']);
            }
        }
    }
typeDefs.js
  type Comment {
        id: ID
        commentText: String!
        author: User!
        createdAt: String
        updatedAt: String 
    }
    type Subscription {
        commentCreated: Comment
    }
Client Side:
import { ApolloClient,InMemoryCache,createHttpLink, ApolloLink, Observable, split } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import { onError } from '@apollo/client/link/error';
import refreshToken from './refreshToken';
import * as SecureStore from 'expo-secure-store';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
import { getMainDefinition } from '@apollo/client/utilities';

// Auth Link for setting the headers with the token
const authLink = setContext(async (_, { headers }) => {
  const token = await SecureStore.getItemAsync('userAccessToken');
  return {
      headers: {
          ...headers,
          authorization: token ? `Bearer ${token}` : "",
      }
  }
});

// Error handling link
const errorLink = onError(({ graphQLErrors,networkError, operation, forward }) => {
  return new Observable(observer => {
    try {
      if (graphQLErrors) {
        //log error
          graphQLErrors.forEach(({ message, locations, path }) => {
            console.log(`[GraphQL Error]: Message: ${message}, Location: ${locations}, Path: ${path}`);
          });
        //END log error

        graphQLErrors.map(async ({ extensions }) => {
          const errorCode = extensions?.code;
          console.log(errorCode);

          if (errorCode === 'UNAUTHENTICATED') {
            try {
              console.log('====================================');
              console.log('arrived');
              console.log('====================================');
              const newAccessToken = await refreshToken();
              console.log('Token Refresh - New Access Token:', newAccessToken);

              // Update headers with the new token
              operation.setContext(({ headers }) => ({
                headers: {
                  ...headers,
                  authorization: newAccessToken ? `Bearer ${newAccessToken}` : '',
                },
              }));

              // Retry the original operation with the new token
              const subscriber = forward(operation).subscribe(observer);

              // Cleanup subscriptions
              return () => subscriber.unsubscribe();
            } catch (refreshError) {
              console.error('Token Refresh - Failed:', refreshError);
              observer.error(refreshError);
            }
          }
        });
      }
      if (networkError) {
        console.log('networkError', networkError);
      }
    } catch (error) {
      console.error('Error in handleAuthenticationError:', error);
      observer.error(error);
    }
  });
});

const token = SecureStore.getItemAsync('userAccessToken');
const wsLink = new GraphQLWsLink(createClient({
  url: 'ws://localhost:4000/graphql',
  options: {
    reconnect: true,
    connectionParams: {
      authToken: token ? `Bearer ${token}` : '',
    },
  },
  on: {
    connected: () => console.log("connected client"),
    closed: () => console.log("closed"),
    error: (err) => console.log("error: " + err.message),
  },
}));

// HTTP connection to the API
const httpLink = createHttpLink({
    uri: "http://localhost:4000/graphql",
    credentials: 'include', // or 'same-origin' based on your needs
});

const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return (
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'subscription'
    );
  },
  wsLink,
  ApolloLink.from([errorLink, authLink.concat(httpLink)]),
);

// Apollo Client setup
const client = new ApolloClient({
    link: splitLink,
    cache: new InMemoryCache(),
});

export default client;
subscription.js
import { gql } from '@apollo/client';

export const COMMENT_CREATED = gql`
  subscription CommentCreated {
    commentCreated {
      commentText
      author {
        id
        fullname
        avatar
      }
    }
  }
`;
todoComponent.tsx
const ViewTodo = ({today}: StackProps) => {

    useSubscription(COMMENT_CREATED, {
      onData: ({ data }) => {
        console.log('data');
        console.log(data);
      },
    });
.........