Open jono-allen opened 1 year ago
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
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.
Right can you confirm the versions for the following:
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
@jono-allen any updates?
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
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
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)
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
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: {
// ...
},
})
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. Usinggraphql-requests
on withnext.js
the request passes. I tried with whatwg-fetch which also fails.Tested using
st-core@5.x
andst-node@14.0.2
withsupertokens-react-native@4.0.1
Prior to "supertokens-node": "12.1.6"
graphql-requests
would work but after 13.x this no longer works