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

Support graphql-request using fetch pollyfills #100

Open
jono-allen opened this issue Jun 8, 2023 · 7 comments
Open

Support graphql-request using fetch pollyfills #100

jono-allen opened this issue Jun 8, 2023 · 7 comments
Labels
enhancement New feature or request

Comments

@jono-allen
Copy link

Continuing from https://discord.com/channels/603466164219281420/1084681154013184040/1084927349562286081

Summary

React-native can authenticate using st-react-native but api calls to protected routes do not get cookies or headers attached to perform authenticated requests. This only affects react-native but not web react apps.

The issue:

Using graphql requests on react-native/expo to perform an authenticated fetch to an express server with supertokens-node, the fetch calls fails to contain the cookies or headers required to authenticate the request.
Using pure "fetch" on react-native, the request passes.
Using graphql-requests on with next.js the request passes.
I tried with whatwg-fetch which also fails.

Tested using [email protected] and [email protected] with [email protected]

Prior to "supertokens-node": "12.1.6" graphql-requests would work but after 13.x this no longer works

@nkshah2
Copy link
Contributor

nkshah2 commented Jun 8, 2023

Hey @jono-allen

Just to confirm, you are saying this used to work with supertokens-node 12.1.6? Im clarifying because graphql-requests should have no relation to the backend SDK

@jono-allen
Copy link
Author

Hey @nkshah2 ,

Sorry I should also mention that we had our managed ST core upgraded as well, I can't remember what version it was on beforehand but we are now on 5.x. (Think it might have been 3.x)
I rolled everything back except core to a known working version of our code that did work before the upgrade and can confirm that was not able to perform any authenticated requests via graphql-request or cross-fetch.

I have switched back to pure react native fetch using the latest version of (react-native, react and node) super tokens sdk and can perform authenticated requests.

@nkshah2
Copy link
Contributor

nkshah2 commented Jun 9, 2023

Right can you confirm the versions for the following:

  • supertokens-node
  • supertokens-react-native
  • graphql-request

If you can please provide versions for when the authenticated requests work and when they don't. Will be really helpful when we try to recreate the issue

@rishabhpoddar
Copy link
Contributor

@jono-allen any updates?

@jono-allen
Copy link
Author

Hey sorry for the delay. Was hoping to put together a example but haven't had time.

Should note that we are using supertokens in a monorepo with next.js and expo

Here are the version when authenticated requests worked on react-native via graphql-requests

"supertokens-react-native": "4.0.0"
"supertokens-auth-react": "0.27.2"
"supertokens-node": "12.1.6",
"graphql-request": "5.1.0",
Uknown supertokens core version (Using managed service)

Here are the version when authenticated requests failed on react-native via graphql-requests

"supertokens-react-native": "4.0.1"
"supertokens-auth-react": "0.32.3",
 "supertokens-web-js": "^0.5.0"
 "supertokens-node": "14.0.2",
"graphql-request": "5.1.0",
Uknown supertokens core version (Using managed service). Requested update to work with latest version

Api calls that worked, then did not work using following graphql api request

import { GraphQLClient } from 'graphql-request'
import { API_BASE_PATH } from 'app/constants'
import { Session } from 'app/services/auth/superTokens'
import { Platform } from 'react-native'
type ErrorAnyResponse = {
  error: Error
  response: Response
  request: Request
}
async function refreshMiddleware(response: unknown) {
  if (Platform.OS === 'web') {
    // web response works as expected
    return
  }
  const result = response as Response | ErrorAnyResponse
  if ('response' in result && result.response.status === 401) {
    console.log('Attempting refresh')
    await Session.attemptRefreshingSession()
    return
  }
  if ('status' in result && result.status === 401) {
    console.log('Attempting refresh')
    await Session.attemptRefreshingSession()
  }
}

const storeApi = new GraphQLClient(`${API_BASE_PATH}/graphql`, {
  credentials: 'include',
  mode: 'cors',
  responseMiddleware: async (response) => {
    await refreshMiddleware(response)
  },
})

storeApi.request(MyQuery) // fails to authenicate

Solution without using graphql requests

Versions

"supertokens-node": "14.0.2",
"supertokens-react-native": "4.0.1"
Removed graphql requests
import { API_BASE_PATH } from 'app/constants'
import type { TypedDocumentNode } from '@graphql-typed-document-node/core'
import type { GraphQLError } from 'graphql/error/GraphQLError.js'

export declare type RequestDocument = string | DocumentNode

import type { DocumentNode, OperationDefinitionNode } from 'graphql'
import { parse, print } from 'graphql'
export interface GraphQLRequestContext<V extends Variables = Variables> {
  query: string | string[]
  variables?: V
}

export interface GraphQLResponse<T = unknown> {
  data?: T
  errors?: GraphQLError[]
  extensions?: unknown
  status: number
  [key: string]: unknown
}

const extractOperationName = (document: DocumentNode): string | undefined => {
  let operationName = undefined

  const operationDefinitions = document.definitions.filter(
    (definition) => definition.kind === `OperationDefinition`
  ) as OperationDefinitionNode[]

  if (operationDefinitions.length === 1) {
    operationName = operationDefinitions[0]?.name?.value
  }

  return operationName
}
export const resolveRequestDocument = (
  document: RequestDocument
): { query: string; operationName?: string } => {
  if (typeof document === `string`) {
    let operationName = undefined

    try {
      const parsedDocument = parse(document)
      operationName = extractOperationName(parsedDocument)
    } catch (err) {
      // Failed parsing the document, the operationName will be undefined
    }

    return { query: document, operationName }
  }

  const operationName = extractOperationName(document)

  return { query: print(document), operationName }
}

export class ClientError extends Error {
  response: GraphQLResponse
  request: GraphQLRequestContext

  constructor(response: GraphQLResponse, request: GraphQLRequestContext) {
    const message = `${ClientError.extractMessage(response)}: ${JSON.stringify({
      response,
      request,
    })}`

    super(message)

    Object.setPrototypeOf(this, ClientError.prototype)

    this.response = response
    this.request = request

    // this is needed as Safari doesn't support .captureStackTrace
    if (typeof Error.captureStackTrace === `function`) {
      Error.captureStackTrace(this, ClientError)
    }
  }

  private static extractMessage(response: GraphQLResponse): string {
    return (
      response.errors?.[0]?.message ??
      `GraphQL Error (Code: ${response.status})`
    )
  }
}

export declare type Variables = {
  [key: string]: any
}
export interface GraphQLClientResponse<Data> {
  status: number
  headers: Headers
  data: Data
  extensions?: unknown
}

function graphqlRequest() {
  const headers = {
    'Content-Type': 'application/json',
  }

  const buildOptions = <V extends Variables = Variables>(
    query: string,
    operationName?: string,
    variables?: V
  ): RequestInit => {
    return {
      method: 'POST',
      headers: headers,
      credentials: 'include',
      mode: 'cors',
      body: JSON.stringify({
        query,
        operationName: operationName,
        variables,
      }),
    }
  }

  return {
    request: async <T, V extends Variables = Variables>(
      document: RequestDocument | TypedDocumentNode<T, V>,
      variables?: V
    ): Promise<T> => {
      const { query, operationName } = resolveRequestDocument(document)

      const options = buildOptions(query, operationName, variables)
      const res = await fetch(`${API_BASE_PATH}/graphql`, options)
      if (!res.ok) {
        const errorResult =
          typeof res === `string`
            ? {
                error: res,
              }
            : res
        throw new ClientError(
          {
            ...errorResult,
            status: res.status,
            headers: res.headers,
          },
          { query, variables }
        )
      }
      const json = await res.json()
      if (json?.data) {
        return json.data
      }
      throw new Error('No data on json response')
    },
  }
}
graphqlRequest.request(MyQuery)

@nkshah2
Copy link
Contributor

nkshah2 commented Jun 15, 2023

Hey @jono-allen

Thanks for the details. Since everything seems to be working with normal fetch its not a bug. We will look into why support for graphql requests broke when moving between versions but since its not a fundamental issue with the SDK itself it will not be a priority for the team.

Leaving this issue open, we'll update here when we start making progress on this

@nkshah2 nkshah2 added the enhancement New feature or request label Jun 15, 2023
@edwinvrgs
Copy link

edwinvrgs commented Nov 17, 2023

I'm facing this issue too, using whatwg-fetch to polifyll fetch for @apollo-client in a Expo app (native and web).

The problem was on the web app, it was not being authenticated properly because of the missing cookie. I was able to get around by doing this:

    SuperTokens.init({
        recipeList: [
            Passwordless.init({}),
            Session.init({
                tokenTransferMethod: "header", // This did the trick
            }),
        ],
        appInfo: {
        // ...
        },
    })

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

4 participants