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

Override fetch for Altas App Services #6165

Closed
wants to merge 4 commits into from

Conversation

kneth
Copy link
Contributor

@kneth kneth commented Sep 29, 2023

What, How & Why?

This closes #6159

☑️ ToDos

  • 📝 Changelog entry
  • 📝 Compatibility label is updated or copied from previous entry
  • 📝 Update COMPATIBILITY.md
  • 🚦 Tests
  • 📦 Updated internal package version in consuming package.jsons (if updating internal packages)
  • 📱 Check the React Native/other sample apps work if necessary
  • 💥 Breaking label has been applied or is not necessary

@coveralls-official
Copy link

coveralls-official bot commented Sep 29, 2023

Coverage Status

coverage: 85.347% (-0.04%) from 85.388% when pulling e721b8f on kneth/custom-network-transport into 190349b on main.

@kneth kneth force-pushed the kneth/custom-network-transport branch from 493a83e to e721b8f Compare September 29, 2023 14:10
@kneth kneth marked this pull request as draft October 2, 2023 07:31
@@ -32,6 +32,7 @@ export type AppConfigurationRelaxed = {
timeout?: number;
multiplexSessions?: boolean;
baseFilePath?: string;
fetchOverride?: any;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My immediate reaction is that the "override" part of this name is redundant as all of these options override default values.

Suggested change
fetchOverride?: any;
fetch?: any;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally, would also be good to match the TS type here to whichever type is used on the SDK-exported type in App.ts.

Comment on lines +28 to +34
const HTTP_METHOD: Record<number, string> = {
[0]: "GET",
[1]: "POST",
[2]: "PUT",
[3]: "PATCH",
[4]: "DELETE",
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it'll make more sense to export this somehow.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, but I have a feeling that we might leak too many implementation details.

Comment on lines +36 to +44
const customFetch = async (request: Request) => {
called = true;
const { url, body, ...rest } = request;
if (typeof request.method == "number") {
rest.method = HTTP_METHOD[request.method];
}
const response = await fetch(url, rest);
return response;
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An alternative could be to implement this as a wrapping of the default fetch from @realm/network-transport.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am split here: part of me wants to wrap it as much as possible, but a part of me wishes to give the user full flexibility.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this solution is good, otherwise we would also have to provide a mechanism to override the default AbortController as well. This basically lets the user override @realm/network-transport.
I guess the crux of the issue, is the user that reported the original issue is using electron and this is using the node bindings instead of the browser bindings to fetch.

@kneth kneth force-pushed the kneth/custom-network-transport branch from e721b8f to 9033d43 Compare October 3, 2023 10:43
@kneth kneth marked this pull request as ready for review October 3, 2023 14:46
@kneth kneth requested a review from takameyer October 3, 2023 14:46
@@ -1 +1 @@
{"REALM_ANONYMIZED_BUNDLE_ID":"tkgif/+3l1e9wStGJp2TOngAK3UcQ2u7OM8ZYJU5JYo="}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be reverted?

* @since 12.3.0
* @experimental This API is experimental and may change or be removed.
*/
fetchOverride?: unknown;
Copy link
Contributor

@takameyer takameyer Oct 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we specify this type to match the NetworkType from network.ts

  fetch(request: Request | binding.Request ): Promise<FetchResponse>;

Copy link
Contributor

@elle-j elle-j Oct 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be similar to a specification, or if that is too specific for a custom fetch, perhaps just a function type such as () => Promise<unknown>. Also, I'd say we should try to not expose bindgen details to the public API.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will there be unintended side effects if the fetch function doesn't match our specifications?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you referring to our NetworkType? Since our TS expects the fetch used (whether it'd be ours or a user-provided one) to be any of the functions defined in the NetworkType, the preferable option (from the way it's set up atm) seems to be fetch(request: Request): Promise<FetchResponse>;. In that case we'd need to export Request and FetchResponse.

But, safest would probably be to ask the user of their intended use (arg and return type) and if fetch(request: Request): Promise<FetchResponse>; would be sufficient for them.

Copy link
Contributor

@takameyer takameyer Oct 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The user in this case just wants to replace node-fetch with window.fetch, since they are running realm in a browser environment in electron.

My main concern is this line. Here there is a transformation of the request happening depending on its contents. If a user is providing a custom fetch, will not doing this transformation cause an issue?

From this observation, perhaps @kraenhansen is correct in that we should directly override the fetch method within @realm/network-transport (a level deeper), and leave this one alone. Then we would also need to provide mechanisms to override the AbortController.

It's probably worth while doing a manual test in React Native, to see if such an override could cause significant issues.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it! 😎 I'll add it to our Round Table and let's discuss more :)

export function overrideFetch(fetch: (request: Request) => Promise<FetchResponse>) {
  baseFetch = fetch;
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My immediate feeling is that I like the declarative API of keeping this a configuration of the App. Possibly providing a way to override the default values for those config values, but then again - how many developers do we expect to have more than one App instance per application?

I like ☝️ over this more procedural overrideFetch function suggestion as that minimize the state in the library, which also has the added benefit of allowing two App instances to fetch using different implementations concurrently.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intention of the overrideFetch suggestion keeps the configuration through App, not for the developer to call this directly. The point being, to keep the internal logic of network:fetch (calls to toFetchRequest), and give the opportunity to override the fetch within the network:fetch method.
I was not proposing to remove the configuration by App, but to clear up that we would be overriding the internal network:fetch logic with the current PR state.

Copy link
Member

@kraenhansen kraenhansen Oct 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@takameyer I don't see how that'll work. The App (or something with access to the fetch passed via the App's configuration) would have to call overrideFetch, passing in the fetch before every single request to the server, as it'll be a shared state between two App instances, that would have to be constantly switched between the two.

Copy link
Contributor

@takameyer takameyer Oct 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kraenhansen Then we should write this in a way that works in the case you are suggesting. The code I wrote above was only an example, but the intention should be clear.

In any case, I think we are misunderstanding each other and would enjoy discussing this further when you return.

Copy link
Contributor

@elle-j elle-j left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great digging, thanks for taking the time to solve this! Left some comments and suggestions to hopefully improve it even more.

@@ -4,7 +4,28 @@
* `Realm.User.providerType` is deprecated, and will be remove in next major version. Use `Realm.User.identities` instead.

### Enhancements
* None
* Expose `Realm.App.Configuration.fetchOverride` to enable implementing a custom [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) function. ([#6159](https://github.com/realm/realm-js/issues/6159))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we should say AppConfiguration.fetchOverride rather than Realm.App.Configuration.fetchOverride due to how users currently can import it. Atm it seems like they can only import it via { AppConfiguration }.

Suggested change
* Expose `Realm.App.Configuration.fetchOverride` to enable implementing a custom [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) function. ([#6159](https://github.com/realm/realm-js/issues/6159))
* Exposed `Realm.App.Configuration.fetchOverride` to enable implementing a custom [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) function. ([#6159](https://github.com/realm/realm-js/issues/6159))

* None
* Expose `Realm.App.Configuration.fetchOverride` to enable implementing a custom [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) function. ([#6159](https://github.com/realm/realm-js/issues/6159))

```js
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
```js
```typescript

@@ -32,6 +32,7 @@ export type AppConfigurationRelaxed = {
timeout?: number;
multiplexSessions?: boolean;
baseFilePath?: string;
fetchOverride?: any;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally, would also be good to match the TS type here to whichever type is used on the SDK-exported type in App.ts.

Comment on lines +28 to +44
const HTTP_METHOD: Record<number, string> = {
[0]: "GET",
[1]: "POST",
[2]: "PUT",
[3]: "PATCH",
[4]: "DELETE",
};

const customFetch = async (request: Request) => {
called = true;
const { url, body, ...rest } = request;
if (typeof request.method == "number") {
rest.method = HTTP_METHOD[request.method];
}
const response = await fetch(url, rest);
return response;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually don't think you need the destructuring nor the typeof check in this case.

Since you're using node-fetch, the request.method will always be a string. Additionally, the destructuring can be removed because the 1st arg to fetch can be the Request itself.

The reason we were doing the typeof dance in our network.ts implementation is because the request could in our case also be a binding.Request (on which the method is an integer).

Suggested change
const HTTP_METHOD: Record<number, string> = {
[0]: "GET",
[1]: "POST",
[2]: "PUT",
[3]: "PATCH",
[4]: "DELETE",
};
const customFetch = async (request: Request) => {
called = true;
const { url, body, ...rest } = request;
if (typeof request.method == "number") {
rest.method = HTTP_METHOD[request.method];
}
const response = await fetch(url, rest);
return response;
};
const customFetch = (request: Request): Promise<Response> => {
called = true;
return fetch(request);
};

Comment on lines +10 to +25
const HTTP_METHOD: Record<number, string> = {
[0]: "GET",
[1]: "POST",
[2]: "PUT",
[3]: "PATCH",
[4]: "DELETE",
};

const customFetch = async (request: Request) => {
const { url, body, ...rest } = request;
if (typeof request.method == "number") {
rest.method = HTTP_METHOD[request.method];
}
const response = await fetch(url, rest); // `fetch` is assumed to be compliant with the Fetch API specification
return response;
};
Copy link
Contributor

@elle-j elle-j Oct 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a PR comment to integration-tests/tests/src/node/custom-fetch.ts explaining why I believe most of this can be simplified.

But also, for this CHANGELOG entry I think it could be good to show only what's necessary since users might have very different implementations of their custom fetch.

Suggested change
const HTTP_METHOD: Record<number, string> = {
[0]: "GET",
[1]: "POST",
[2]: "PUT",
[3]: "PATCH",
[4]: "DELETE",
};
const customFetch = async (request: Request) => {
const { url, body, ...rest } = request;
if (typeof request.method == "number") {
rest.method = HTTP_METHOD[request.method];
}
const response = await fetch(url, rest); // `fetch` is assumed to be compliant with the Fetch API specification
return response;
};
const customFetch = (request: Request): Promise<Response> => {
// `fetch` is assumed to be compliant with the Fetch API specification
return fetch(request);
};


it("custom fetch is called", async function (this: Mocha.Context & AppContext & RealmContext) {
let user;
called = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since called is shared among all tests in this suite (if more tests were to be added), perhaps we could also set it to false in a before/afterEach to not be too dependent on the need to know that it should always be set to false first?

Comment on lines +24 to +28
import { baseUrl, Response } from "../../utils/import-app";
import { select } from "../../utils/select";
import { buildAppConfig } from "../../utils/build-app-config";
import { Fetch, Request } from "@realm/network-transport";
import { fetch } from "../../utils/fetch";
Copy link
Contributor

@elle-j elle-j Oct 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the added imports (Response, Fetch, Request, fetch) can safely be removed. They don't seem to be used anywhere here.

* @since 12.3.0
* @experimental This API is experimental and may change or be removed.
*/
fetchOverride?: unknown;
Copy link
Contributor

@elle-j elle-j Oct 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be similar to a specification, or if that is too specific for a custom fetch, perhaps just a function type such as () => Promise<unknown>. Also, I'd say we should try to not expose bindgen details to the public API.

Comment on lines 288 to +291

if (fetchOverride) {
inject({ fetch: fetchOverride } as NetworkType);
}
Copy link
Contributor

@elle-j elle-j Oct 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we decide to add a more explicit type than any or unknown to the fetchOverride in the app config (I think at least specifying that it should be a function could be good), then what do you think of adding an explicit check for undefined here? This way, we can let users know if they have provided an invalid falsy value in addition to an invalid truthy value by asserting that it's a function.

Suggested change
if (fetchOverride) {
inject({ fetch: fetchOverride } as NetworkType);
}
if (fetchOverride !== undefined) {
assert.function(fetchOverride, "fetchOverride");
inject({ fetch: fetchOverride } as NetworkType);
}

@@ -60,5 +60,5 @@ export const network: NetworkType = {
};

export function inject(injected: NetworkType) {
Object.freeze(Object.assign(network, injected));
Object.assign(network, injected);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did using .freeze() cause an issue here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless this inject called multiple times, the freeze shouldn't be an issue.

Copy link
Member

@kraenhansen kraenhansen Oct 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah - my suggestion (to @kneth) to remove the freeze was only if we choose to expose the inject function to end-users as a means of overriding the fetch implementation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha 🙂

@kneth kneth closed this May 23, 2024
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jun 22, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Starting from version 12 it is not possible to override the fetch API used by SDK
4 participants