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

Fetch statusText polyfill #5530

Merged
merged 2 commits into from
Mar 3, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 31 additions & 24 deletions packages/realm-network-transport/src/DefaultNetworkTransport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
////////////////////////////////////////////////////////////////////////////

import { makeRequestBodyIterable } from "./IterableReadableStream";
import { deriveStatusText } from "./status-text";
import type {
NetworkTransport,
Request,
Expand All @@ -36,6 +37,29 @@ export class DefaultNetworkTransport implements NetworkTransport {
"Content-Type": "application/json",
};

private static createTimeoutSignal(timeoutMs: number | undefined) {
if (typeof timeoutMs === "number") {
const controller = new DefaultNetworkTransport.AbortController();
// Call abort after a specific number of milliseconds
const timeout = setTimeout(() => {
controller.abort();
}, timeoutMs);
return {
signal: controller.signal,
cancelTimeout: () => {
clearTimeout(timeout);
},
};
} else {
return {
signal: undefined,
cancelTimeout: () => {
/* No-op */
},
};
}
}

Comment on lines +40 to +62
Copy link
Member Author

Choose a reason for hiding this comment

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

I just moved this up to be a static member, since it didn't need access to the instance.

constructor() {
if (!DefaultNetworkTransport.fetch) {
throw new Error("DefaultNetworkTransport.fetch must be set before it's used");
Expand Down Expand Up @@ -68,42 +92,25 @@ export class DefaultNetworkTransport implements NetworkTransport {

public async fetch<RequestBody = unknown>(request: Request<RequestBody>): Promise<FetchResponse> {
const { timeoutMs, url, ...rest } = request;
const { signal, cancelTimeout } = this.createTimeoutSignal(timeoutMs);
const { signal, cancelTimeout } = DefaultNetworkTransport.createTimeoutSignal(timeoutMs);
try {
// Awaiting the response to cancel timeout on errors
const response = await DefaultNetworkTransport.fetch(url, {
...DefaultNetworkTransport.extraFetchOptions,
signal, // Used to signal timeouts
...rest,
});
// A bug in the React Native fetch polyfill leaves the statusText empty
if (response.statusText === "") {
const statusText = deriveStatusText(response.status);
// @ts-expect-error Assigning to a read-only property
response.statusText = statusText;
}
// Wraps the body of the request in an iterable interface
return makeRequestBodyIterable(response);
} finally {
// Whatever happens, cancel any timeout
cancelTimeout();
}
}

private createTimeoutSignal(timeoutMs: number | undefined) {
if (typeof timeoutMs === "number") {
const controller = new DefaultNetworkTransport.AbortController();
// Call abort after a specific number of milliseconds
const timeout = setTimeout(() => {
controller.abort();
}, timeoutMs);
return {
signal: controller.signal,
cancelTimeout: () => {
clearTimeout(timeout);
},
};
} else {
return {
signal: undefined,
cancelTimeout: () => {
/* No-op */
},
};
}
}
}
77 changes: 77 additions & 0 deletions packages/realm-network-transport/src/status-text.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
////////////////////////////////////////////////////////////////////////////
//
// Copyright 2023 Realm Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////

const HTTP_STATUS_TEXTS: Record<number, string | undefined> = {
100: "Continue",
101: "Switching Protocols",
102: "Processing",
200: "OK",
201: "Created",
202: "Accepted",
203: "Non-Authoritative Information",
204: "No Content",
205: "Reset Content",
206: "Partial Content",
300: "Multiple Choices",
301: "Moved Permanently",
302: "Found",
303: "See Other",
304: "Not Modified",
307: "Temporary Redirect",
308: "Permanent Redirect",
400: "Bad Request",
401: "Unauthorized",
402: "Payment Required",
403: "Forbidden",
404: "Not Found",
405: "Method Not Allowed",
406: "Not Acceptable",
407: "Proxy Authentication Required",
408: "Request Timeout",
409: "Conflict",
410: "Gone",
411: "Length Required",
412: "Precondition Failed",
413: "Payload Too Large",
414: "URI Too Long",
415: "Unsupported Media Type",
416: "Range Not Satisfiable",
417: "Expectation Failed",
418: "I'm a teapot",
Copy link
Contributor

Choose a reason for hiding this comment

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

🫖

Copy link
Contributor

Choose a reason for hiding this comment

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

The HTTP 418 I'm a teapot client error response code indicates that the server refuses to brew coffee because it is, permanently, a teapot. A combined coffee/tea pot that is temporarily out of coffee should instead return 503.

-- Hyper Text Coffee Pot Control Protocol

422: "Unprocessable Entity",
425: "Too Early",
426: "Upgrade Required",
429: "Too Many Requests",
431: "Request Header Fields Too Large",
451: "Unavailable For Legal Reasons",
500: "Internal Server Error",
501: "Not Implemented",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Gateway Timeout",
505: "HTTP Version Not Supported",
506: "Variant Also Negotiates",
507: "Insufficient Storage",
508: "Loop Detected",
510: "Not Extended",
511: "Network Authentication Required",
Comment on lines +23 to +75
Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add that as a comment in the code?

Copy link
Member Author

Choose a reason for hiding this comment

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

Done 👍

};

export function deriveStatusText(status: number): string | undefined {
return HTTP_STATUS_TEXTS[status];
}
8 changes: 4 additions & 4 deletions packages/realm/src/app-services/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,11 +268,11 @@ export class User<
serviceName,
);

const { body, ok, status, statusText } = await network.fetch(request);
assert(ok, `Request failed: ${statusText} (${status})`);
assert(body, "Expected a body in the response");
const response = await network.fetch(request);
assert(response.ok, () => `Request failed: ${response.statusText} (${response.status})`);
elle-j marked this conversation as resolved.
Show resolved Hide resolved
assert(response.body, "Expected a body in the response");

return body;
return response.body;
}

/**
Expand Down