relay-tools / react-relay-network-modern Goto Github PK
View Code? Open in Web Editor NEWRelay Modern Network Layer with middlewares — cache, auth, retry, batch, logger, SSR
License: MIT License
Relay Modern Network Layer with middlewares — cache, auth, retry, batch, logger, SSR
License: MIT License
I have a refetch query in modern that is being run on two instances of the same component, each with different variables on the request. Within the batch middleware, the relayReqId is identical and the same name as the refetch query. However, the fetch variables are different, but any subsequent request in the same batch that has different variables seems to only get treated as a dup and not fetched.
FYI.
Oh, and you are a badass. Great work, brother.
I may just be really dense, but hopefully this feedback is helpful. I've been using Relay with a fetchQuery
function as detailed in the Relay docs. Now I'd like to add an Authorization header and came across this project to do so. Given it's middleware, I'd expect to set it up to wrap my fetchQuery
function, but there's no indication in the docs how to do that. I came across an answer on StackOverflow that suggests using urlMiddleware
in place of fetchQuery
. If that's really the intended approach, I think the docs could be clearer in indicating that.
Add option to pass a custom fetch method.
This is mostly for unit test use case where I need to use a mocked instance of fetch.
For better test isolation I need to pass a new fetch mock in each of my tests.
Hi,
After upgrading from version 2.3.3 to 2.3.4, I'm starting to get this error intermittently when fetching a query:
TypeError: undefined is not a function (evaluating '(deferrableSelections \|\| new Set())[typeof Symbol === "function" ? Symbol.iterator : "@@iterator"]()')
--
Note : I'm using it in a react native project
Any ideas? Thanks.
Getting
Error: Cannot find module 'core-js/modules/es6.object.define-property'
when running my server.
Installed:
Installing [email protected]
fixes the issue, but that's an obsolete dependency, would really like to avoid having to use it.
Hi, in my project I am using
When I use the RelayNetworkLayer
I get this error.
This is my code
import { Environment, RecordSource, Store } from 'relay-runtime';
import 'regenerator-runtime/runtime';
import { RelayNetworkLayer, authMiddleware } from 'react-relay-network-modern';
const network = new RelayNetworkLayer([
authMiddleware({
token: () => 'My Token',
}),
next => async req => {
req.fetchOpts.url = 'Some url';
req.fetchOpts.credentials = 'include';
const res = await next(req);
return res;
},
]);
const source = new RecordSource();
const store = new Store(source);
const environment = new Environment({
network,
store,
});
export default environment;
The environment
created by new Environment(...)
has the following shape
RelayModernEnvironment {
_network {
fetchFn: (operation, variables, cacheConfig, uploadables)
}
}
Fails to load due to regeneratorRuntime not being installed. I'm planning on using react-relay-network-modern in a next.js app with SSR and on node 8 this fails due to regeneratorRuntime not being included. I'd like to use native async await when on node 8. Thoughts?
Here https://github.com/relay-tools/react-relay-network-modern/blob/master/src/middlewares/auth.js#L65-L69 when we retry request after token refresh we don't take into account allowEmptyToken
.
It is inconsistent with logic here https://github.com/relay-tools/react-relay-network-modern/blob/master/src/middlewares/auth.js#L37-L48
Should allowEmptyToken
be taken into account on retry?
Any plans of supporting persistent queries?
https://facebook.github.io/relay/docs/en/persisted-queries.html
Jayden Seric wrote an amazing spec for file upload: https://github.com/jaydenseric/graphql-multipart-request-spec
It has an implementation for most servers express
, koa
, also used in Apollo and Prisma. Will be nice if somebody takes care about writing a new fileUpload
middleware for this package.
The TypeScript definition for RelayResponse
is missing definitions for all of its fields. These are available in the Flow definitions, so I think this was an oversight rather than a deliberate action. Currently, it's making writing custom middleware difficult because there's no way to inspect the response object without casting the object as any
.
Hi I think there's a bug with the Auth middleware where the request param for Authorisation keeps being lowercased. I've tried this for both default values and supplying a header option to the middleware. I'm on 2.3.3
This is how it looks like in the chrome Network inspector:
Below is the following code where I am setting up the middleware. Is there anything here I am missing?
const network = new RelayNetworkLayer(
[
urlMiddleware({
url: req => Promise.resolve(Config.getServerUrl()),
}),
authMiddleware({
token: () => {
let authService = new AuthService(Config.getAuthConfig())
return authService.getAccessToken()
},
header: 'Authorization'
})
],
middlewareOpts
);
the following function in fetchWithMiddleware converts http response to an RRNLRequestError
if resonse status is abover 400.
const convertResponse: (next: MiddlewareRawNextFn) => MiddlewareNextFn = (next) => async (req) => {
const resFromFetch = await next(req);
const res = await RelayResponse.createFromFetch(resFromFetch);
if (res.status && res.status >= 400) {
throw createRequestError(req, res);
}
return res;
};
how do you catch 401 reponse? I'd like to redirect user to login page when user details API returns 401
const { userProfile } = useLazyLoadQuery<MainLayout_GetUserProfileQuery>(
graphql`
query MainLayout_GetUserProfileQuery {
userProfile {
...AvatarDropdown_currentUser
}
}
`,
{},
{
fetchPolicy: "network-only",
}
);
I got a error like following which caused the whole app crached.
This way doesn't work, the error
somehow is a promise, not RRNLRequestError.
try {
const { userProfile } = useLazyLoadQuery<MainLayout_GetUserProfileQuery>(
graphql`
query MainLayout_GetUserProfileQuery {
userProfile {
...AvatarDropdown_currentUser
}
}
`,
{},
{
fetchPolicy: "network-only",
}
);
} catch(error) {
const {
res
} = error as { res: RelayNetworkLayerResponse };
console.log(error);
// if (res.status === 401) {
// Router.push('/login');
// }
return null;
}
If the same query is made sequentially with different variables, batchMiddleware will cause the second request to be ignored and return results from the first request.
I'm aborting inside beforeRetry
. However, when the requests were batched, I get the following error:
Uncaught (in promise) RRNLRetryMiddlewareError: Aborted in beforeRetry() callback
I have set the noThrow
option to true
.
beforeRetry: ({forceRetry, abort, delay, attempt, lastError, req}) => {
...
if (attempt === 5) {
return abort();
}
...
}
I'm trying to centralize my exception reporting with react-relay-network-modern and haven't found a very good way to do so yet within the confines of either the existing middleware or providing my own custom middleware. In my case, I'm looking to report GraphQL errors to Sentry, but any exception reporting or log aggregation service would run into the same issue. I'm deliberately not looking to add logging code to every QueryRenderer
because it's too easy to miss a case and any change in logging requires updating it in N places.
There are two basic error classes I'd like to capture: fetch
errors and GraphQL responses that come back with an errors
field. As far as I can tell, the only way to centrally handle the fetch
errors is with custom middleware:
next => async req => {
Sentry.setTag('graphql_query', req.getID())
try {
return await next(req)
} catch (e) {
Sentry.captureException(e)
throw e
}
}
That part works fine, although I also get errors about requests being canceled if a user goes to a different page while the query is still running. So a real solution will require a bit more filtering.
Logging the errors
field, however, is proving to be a lot more complicated. It exists as a field on the response object that's later turned into a RRNLRequestError
. The conversion to an error appears to happen outside the middleware stack, so the catch
statement from my custom middleware doesn't catch it.
The errors
field will populate on the response object, so I could do something like:
next => async req => {
Sentry.setTag('graphql_query', req.getID())
try {
const res = await next(req)
if (res.errors) {
Sentry.captureException(res.errors)
}
return res
} catch (e) {
Sentry.captureException(e)
throw e
}
}
But, res.errors
isn't really an exception, so that makes the logging in Sentry a bit weird. Since errors
is typed as any
and has no formatting functions that I could find, trying to treat it as a string and use Sentry.captureMessage(res.errors.toString())
doesn't produce useful output.
Another option is using the errorMiddleware
, like so:
errorMiddleware({
logger: (message: string) => { Sentry.captureMessage(message) }
})
I didn't really want to split the error handling up, but I could live with that. The real problem with this option is the message
argument really expects to be used with console
. The docs mention the logger
value is "log function (default: console.error.bind(console)
)", but if you use anything other than console
you end up with a bunch of noisy formatting codes (%c
) with no way to disable them. However, at least you have an otherwise nicely formatted message.
It'd be nice if I could invoke that formatter code from my custom middleware and handle logging of res.errors
, but that formatting code is private to errorMiddleware
.
The final option is to just handle RRNLRequestError
in every QueryRenderer
. I don't really like this option because it's easy to miss a logging statement and it litters the exception handling code around. But, with this approach you get a nicely formatted error message and a stack trace. Perhaps exposing formatGraphQLErrors
and createRequestError
is the best option?
I'm happy to provide a PR with documentation enhancement. I just don't know what the best way to centralize exception handling currently is. Any guidance would be much appreciated.
We used the relay middleware to help us cache some queries. One of being a query called ViewerIdQuery, it just fetches the id on the viewer. For some reason our help abort this query keeps getting aborted. What would cause a query to be aborted? If we cache it, should it still be reported that it's aborted?
LOG [RELAY-NETWORK] Aborted
LOG [RELAY-NETWORK] will retry in 1000 milliseconds
LOG [RELAY-NETWORK] Cancelled ViewerIdQuery
LOG [trace] Rollbar.error
[AbortError: Aborted]
I'm running into issues using subscriptions-transport-ws
with this. The subscriptions never close and don't seem to be getting data.
import { SubscriptionClient } from 'subscriptions-transport-ws'
// ...
const client = new SubscriptionClient('/api/graphql', {
reconnect: true
})
function getSubscribeFn(client) {
return (operation, variables) => {
return client.request({ query: operation.text, variables, operationName: operation.name })
}
}
// ...
{
subscribeFn: getSubscribeFn(client)
}
}
It would be nice to export a subscribeFn
to properly handle subscriptions.
It could easily just accept a SubscriptionClient
instance.
Have there been any attempts to upgrade to Relay v4.0.0? The main change is container parameters. I'm not sure if/how this impacts this library.
We would like to return cached data to React, but also fetch new data in the background and update accordingly when the new data arrives.
Our specific circumstances are that our queries can take a long time to respond and the data is used in visualizations. We would like our visualisations to appear immediately using old data, then update to show new data when it arrives.
Ideally, we'd then like to cache in localStorage, so visuals can be seen both offline and instantly when first visiting the site again.
Current cache using RelayQueryResponseCache
will always show old data, until the cache expires.
The undocumented dataFrom="STORE_THEN_NETWORK"
on the <QueryRenderer>
doesn't work if you have multiple stores, and also doesn't support offline/page reload caching.
This feels like something that should/could be implemented at the network layer, but I've had no luck figuring out how to do.
Any suggestions, would be happy to make a PR if it is useful.
once queries have been queued due to the batch timeout. Create a new queue and wait for:
Promise.all(oldBatch, sleep(batchTimeout))
before issuing the next batch.
This should also apply to retries etc. Eg if a request has failed due to a retry the next trail should include new batched requests.
How can I serialize the json for SSR? Right now I have something like:
<script>
window.__RELAY_PAYLOADS__ = ${serialize(fetcher, { isJSON: true })};
</script>
And fetcher.toJson()
simply returns the payloads. But I don't see any way of doing it with this network library.
To use Relay Polling (poll in cache config
) we have to wrap the promise in fetch function into an observable.
I was able to do so this way :
import {
Environment,
RecordSource,
Store,
Network,
Observable,
} from 'relay-runtime';
import {
RelayNetworkLayer,
...
} from 'react-relay-network-modern/es';
const network = new RelayNetworkLayer([
....
]);
const fetchFunction = (...args) =>
Observable.create(sink => {
network.fetchFn(...args).then(
data => {
sink.next(data);
sink.complete();
},
e => {
sink.error(e);
sink.complete();
},
);
});
const relayEnvironment = new Environment({
network: Network.create(fetchFunction, network.subscribeFn),
store: new Store(new RecordSource()),
});
export default relayEnvironment;
Perhaps it should be supported natively in this plugin ?
where possible the server should be able to return an LD-JSON stream of resolved queries. The batch middleware should be able to resolve individual queries as their batched result arrives.
I want to be able to customize the error message, when i use beforeRetry
, so that I can distinguish between different terminations and make different behaviors.
It is easy to implement like this:
abort = msg => {
if (delayId) {
clearTimeout(delayId);
delayId = null;
}
reject(new RRNLRetryMiddlewareError(msg || 'Aborted in beforeRetry() callback'));
};
These functions (createRequestError
and formatGraphQLErrors
) don't have return type annotations. When using this library with typescript in strict mode, builds fail.
There are some problem:
If refresh token failed, the auth middleware never retry to refresh token.
Can I have any way to solve this problem?
react-relay-network-modern/src/middlewares/auth.js
Lines 33 to 71 in 47806aa
the tokenRefreshInProgress
never reset state if refresh token failed
(I've created pull request here #14 and let's discuss it here.)
My issue is that fetch doesn't send with credentials option when both url and batch middleware are used. Here is the code.
const network = new RelayNetworkLayer([
cacheMiddleware({
size: 100,
ttl: 900000,
}),
urlMiddleware({
credentials: 'include', // <----------- credentials here is not set on fetch
url: 'http://localhost:3000/graphql',
}),
batchMiddleware({
batchUrl: 'http://localhost:3000/graphql/batch',
batchTimeout: 10,
}),
])
When I look at the code, it seems RelayRequestBatch ignores fetchOpts that is set from urlMiddleware (code here).
The code in PR just checks the credentials option and set it accordingly. Not sure if it's the best way to do it tho. What do you think?
Hi there, been trying to find out but got lost, wondering if anyone here can help or point the right direction.
Given a Relay QueryRenderer, and given a 401 response status error, is there a way I can put a middleware before the request goes back, seems no responses are being called in any middleware and process errors inside render are not very wise.
Could someone replay with what solutions you doing regarding this? I could hide the status code and make it 200, could also provide errors with empty data, but I think this isn't the right direction.
Thanks in advance.
On Modern Relay we pass uploadables in fetchQuery
fetchQuery(operation, variables, cacheConfig, uploadables) {
//....
}
But how to do it in RRNM ?
Here is my environment...
const network = new RelayNetworkLayer(middlewares, options);
const store = new Store(new RecordSource());
const environment = new Environment({
network,
store,
});
this way the /es
builds will still have all their type info
The CacheMiddleware is missing from the d.ts file.
This has been missing as of version 2.7.0
when TypeScript typing was introduced.
I opened a PR on my forked repo to fix it: #58
We just implemented the batch middleware, and are experiencing a case where our auto-complete queries don't show a result because all requests were canceled (e.g. when someone types really fast, React makes a request on each key press, so that can add up to many queries that are issued and canceled until the last one completes).
When there is a series of requests, it makes sense that any previous (incomplete) requests would be canceled before making the same request again. However, with the batch middleware, even the last request is canceled (and no final request is made).
My guess is this is related to the setTimeout
used to collect requests to batch. When it's just one query, even though it should process that query as if it wasn't batched, there is still some sort of timing issue.
Thoughts on what might cause that?
Latest's version fixes this issue
ReferenceError: module is not defined
(anonymous function)
https://pwa.test:3000/static/js/0.chunk.js:87327:30
Module../node_modules/react-relay-network-modern/node_modules/extract-files/lib/ReactNativeFile.mjs
Hi!
I was wondering if it would be possible to somehow support something like https://github.com/relay-tools/fetch-multipart-graphql in order to support the @defer directive. Would that change be too invasive on this code base, or would it be possible to implement in a sane way?
Thanks!
I'm trying to make multipart/form-data
responses work with this library. In short, I have wired the server so that in some cases, it will respond with a multipart/form-data
response containing some binary representation of scalars.
It would be cool to be able to override how RelayResponse.createFromFetch
decodes the responses, namely instead of only using .json
to be able to change the logic in there to use .formData
in some cases.
Maybe this is already possible, but I cannot figure it out after reading through the source code.
What is the reasoning behind the check on 112 in middlewares/retry.js
:
if (e && e.res && o.retryOnStatusCode(e.res.status, o.req, e.res)) {
In a case where the request does not reach the server (network connection problem, etc.), this check fails and the error is rethrown instead of a making another retriable request. Have you considered handling such cases?
I added into my js file
import 'regenerator-runtime'
But as soon as I add a custom middleware I have this error. If I remove custom middleware from the code it works (I do not have this error).
frontend_1 | /opt/frontend/node_modules/react-relay-network-modern/lib/RelayResponse.js:41
frontend_1 | var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(res) {
frontend_1 | ^
frontend_1 |
frontend_1 | ReferenceError: regeneratorRuntime is not defined
frontend_1 | at /opt/frontend/node_modules/react-relay-network-modern/lib/RelayResponse.js:41:50
frontend_1 | at /opt/frontend/node_modules/react-relay-network-modern/lib/RelayResponse.js:94:6
frontend_1 | at Object.<anonymous> (/opt/frontend/node_modules/react-relay-network-modern/lib/RelayResponse.js:132:2)
frontend_1 | at Module._compile (module.js:643:30)
frontend_1 | at Object.Module._extensions..js (module.js:654:10)
frontend_1 | at Module.load (module.js:556:32)
frontend_1 | at tryModuleLoad (module.js:499:12)
frontend_1 | at Function.Module._load (module.js:491:3)
frontend_1 | at Module.require (module.js:587:17)
frontend_1 | at require (internal/module.js:11:18)
Update:
I found out that if I do not use async I do not have this error.
it works:
next => req => {
return next(req)
}
it retuns error:
next => async req => {
const res = await next(req)
return res
}
There might be a reason why this wasn't included, but it would be nice to be able to pass an array of query IDs into batchMiddleware
that we want to fire off separately.
I would think this is a reasonably common use-case. For example, we have a home screen query that we'd like returned as soon as possible so we can show the user data, whereas it's nice for the other screens' queries to be batched up. I've been trying to build my own additional middleware to do this, but haven't succeeded so far.
Is there a reason why this is a bad idea?
I'm intermittently observing the following error. I've checked my queries but couldn't find any recursion issue. Any ideas?
In RelayModernSelector.js:137:
Uncaught TypeError: Converting circular structure to JSON
at JSON.stringify (<anonymous>)
at getDataIDsFromObject (RelayModernSelector.js:137)
at getDerivedStateFromProps (ReactRelayFragmentContainer.js:116)
at applyDerivedStateFromProps (react-dom.development.js:11151)
at updateClassInstance (react-dom.development.js:11637)
at updateClassComponent (react-dom.development.js:13045)
at beginWork (react-dom.development.js:13715)
at performUnitOfWork (react-dom.development.js:15741)
at workLoop (react-dom.development.js:15780)
at HTMLUnknownElement.callCallback (react-dom.development.js:100)
at Object.invokeGuardedCallbackDev (react-dom.development.js:138)
at invokeGuardedCallback (react-dom.development.js:187)
at replayUnitOfWork (react-dom.development.js:15194)
at renderRoot (react-dom.development.js:15840)
at performWorkOnRoot (react-dom.development.js:16437)
at performWork (react-dom.development.js:16358)
at performSyncWork (react-dom.development.js:16330)
at requestWork (react-dom.development.js:16230)
at scheduleWork$1 (react-dom.development.js:16096)
at Object.enqueueSetState (react-dom.development.js:11185)
at Connect../node_modules/react/cjs/react.development.js.Component.setState (react.development.js:273)
at Connect.onStateChange (connectAdvanced.js:205)
at dispatch (redux.js:221)
at createMatchEnhancer.js:31
at createTransitionHookMiddleware.js:246
at createLocationMiddleware.js:32
at createHistoryMiddleware.js:47
at createLocationMiddleware.js:32
at createTransitionHookMiddleware.js:246
at ensureLocationMiddleware.js:44
at Object.onResolveMatch (redux.js:449)
at BaseRouter._callee$ (createBaseRouter.js:222)
at tryCatch (runtime.js:62)
at Generator.invoke [as _invoke] (runtime.js:296)
at Generator.prototype.(anonymous function) [as next] (http://localhost:3000/static/js/bundle.js:14742:21)
at step (asyncToGenerator.js:17)
at asyncToGenerator.js:28
For some nodes in my graph I'd like them to always update but temporarily show stale data while I'm loading them.
I'm trying to follow the example from the README, but TypeScript won't allow the example code to work with relay-runtime 7.0.0:
TypeScript error in /home/nirvdrum/dev/workspaces/my_project/src/components/Auth0RelayEnvironmentProvider.tsx(51,33):
Type 'RelayNetworkLayer' is not assignable to type 'Network'.
Types of property 'execute' are incompatible.
Type 'import("/home/nirvdrum/dev/workspaces/my_project/node_modules/react-relay-network-modern/node_modules/@types/relay-runtime/index").ExecuteFunction' is not assignable to type 'import("/home/nirvdrum/dev/workspaces/my_project/node_modules/@types/relay-runtime/lib/network/RelayNetworkTypes").ExecuteFunction'.
Property 'concat' is missing in type 'import("/home/nirvdrum/dev/workspaces/my_project/node_modules/react-relay-network-modern/node_modules/@types/relay-runtime/index").Observable<import("/home/nirvdrum/dev/workspaces/my_project/node_modules/react-relay-network-modern/node_modules/@types/relay-runtime/index").GraphQLResponse>' but required in type 'import("/home/nirvdrum/dev/workspaces/my_project/node_modules/@types/relay-runtime/lib/network/RelayObservable").RelayObservable<import("/home/nirvdrum/dev/workspaces/my_project/node_modules/@types/relay-runtime/lib/network/RelayNetworkTypes").GraphQLResponse>'. TS2322
49 |
50 | const store = new Store(new RecordSource())
> 51 | const env = new Environment({ network, store })
Please drop a line if you use RRNM.
In which apps, which middlewares are you using, maybe you wrote some custom middleware?
Tnx.
It would be nice if there was a full, runnable example showing how to work with SSR. For example, in a minimal example such as
import 'isomorphic-fetch'
import 'regenerator-runtime/runtime'
import { Environment, RecordSource, Store } from 'relay-runtime'
// NOTE:
// This import currently doesn't work out of the box, as the source is not transpiled and
// `import` statements still exist in output, which Node 8 does not support.
const RRNL = require('react-relay-network-modern/node8')
const network = new RRNL.RelayNetworkLayer([
urlMiddleware({ url: "https://metaphysics-staging.artsy.net" }),
])
const source = new RecordSource()
const store = new Store(source)
const relayEnvironment = new Environment({
network,
store
})
How does one then wire up SSR? Any links / examples / guides would be appreciated.
Hi
I have a response in the following shape:
{"data":null,"errors":[{"message":"The request is invalid.","code":400,"meta":{"name":"already exists"},"locations":[{"line":4,"column":3}],"path":["createAttribute"]}]}
Is it possible to get meta
object instead of string message (The request is invalid
in this case) as a first argument in onError
callback when commiting a mutation?
Hey guys, thanks for this awesome lib!
I am facing a problem with cookies.
I have a GraphQL Server running on port 5001 and a Web running on ports 7002 (Server) and 7001 (Client). Everything works fine until I try to call GraphQL server directly from client, without passing through Web Server.
For example, if I call a login mutation from Web Client to GraphQL Server, the GraphQL Server sets cookies, but they aren't persisted. But if I call a login mutation from Web Client to Web Server and then it proxies to the GraphQL Server, the GraphQL Server sets cookies and their persisted.
So basic the problem I've noticed is that, when not using Web Server, cookies are not persisted.
Any ideas? I am using urlMiddleware
to set GraphQL URL.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.