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

feat: implement net.fetch #36733

Merged
merged 54 commits into from
Feb 20, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
bc75a67
feat: implement net.fetch
nornagon Dec 22, 2022
c88806a
Merge branch 'main' into net-fetch
nornagon Jan 23, 2023
41b2210
handle abort
nornagon Jan 25, 2023
befbe6e
better types
nornagon Jan 26, 2023
64ad0ca
support cache, referrerPolicy, etc
nornagon Jan 26, 2023
20ac105
Merge branch 'main' into net-fetch
nornagon Jan 26, 2023
b43ace8
kick ci
nornagon Jan 26, 2023
6057958
doc links
nornagon Jan 26, 2023
c0f23c3
add test
nornagon Jan 26, 2023
11de4bc
hack ts defs
nornagon Jan 26, 2023
681597f
refactor: simplify events
nornagon Feb 1, 2023
2104b3d
more cleanup
nornagon Feb 1, 2023
6bdc04d
drop unneeded headers
nornagon Feb 1, 2023
d7d044b
simplify ui_event
nornagon Feb 1, 2023
fcfc826
remove EmitCustomEvent
nornagon Feb 1, 2023
e484046
correctly initialize PreventableEvent
nornagon Feb 1, 2023
7ace33f
remove _reply and _throw
nornagon Feb 1, 2023
dff55f0
every PreventableEvent has a sender
nornagon Feb 1, 2023
a9c965d
Merge remote-tracking branch 'origin/main' into net-fetch
nornagon Feb 2, 2023
8234acf
fix typescript-definitions
nornagon Feb 2, 2023
2f4909b
build: use new type defs
MarshallOfSound Feb 2, 2023
dbf6b90
fix types
nornagon Feb 2, 2023
aaf9379
fix referrer test
nornagon Feb 2, 2023
05efb81
isReadable
nornagon Feb 2, 2023
4e0d1f1
fix types in test
nornagon Feb 2, 2023
b4b4e81
fix types for webContents ipc-message events
nornagon Feb 6, 2023
a1764ac
drop as any
nornagon Feb 6, 2023
58c9dc2
Merge remote-tracking branch 'origin/main' into event-refactor
nornagon Feb 6, 2023
0491ab4
Merge remote-tracking branch 'origin/main' into net-fetch
nornagon Feb 6, 2023
79afe45
Merge remote-tracking branch 'origin/main' into net-fetch
nornagon Feb 7, 2023
3829c76
add ses.fetch
nornagon Feb 7, 2023
4b233bd
remove cache = ""
nornagon Feb 7, 2023
c071c8b
bump typescript-definitions
nornagon Feb 7, 2023
cef954e
fix types
nornagon Feb 7, 2023
7c26527
Merge branch 'event-refactor' into net-fetch
nornagon Feb 7, 2023
f87690d
fail silently when SendReply is called multiple times
nornagon Feb 7, 2023
2e95959
add sender to web-contents-created
nornagon Feb 8, 2023
042dc49
rename ReplySender -> ReplyChannel
nornagon Feb 8, 2023
d97e86f
rename PreventableEvent to Event
nornagon Feb 8, 2023
3d1cc34
CustomEvent -> Event
nornagon Feb 8, 2023
ca6acfc
forgot some replySender->Channel
nornagon Feb 8, 2023
f9a7237
rename files
nornagon Feb 8, 2023
fe5a2ad
remove Event.sender for most events
nornagon Feb 8, 2023
12cdf0a
drop CreateEvent for Event::New
nornagon Feb 8, 2023
714fc66
update ts-defs
nornagon Feb 9, 2023
94330f4
Merge remote-tracking branch 'origin/main' into event-refactor
nornagon Feb 9, 2023
408ef5b
fix types again
nornagon Feb 10, 2023
3b1e86b
fix types
nornagon Feb 13, 2023
45c15d9
Merge branch 'main' into event-refactor
nornagon Feb 13, 2023
ff0a824
fix type again
nornagon Feb 13, 2023
973b8ce
Merge branch 'event-refactor' into net-fetch
nornagon Feb 13, 2023
5c399a3
move ClientRequest to a separate file
nornagon Feb 13, 2023
4a11e4e
Merge remote-tracking branch 'origin/main' into net-fetch
nornagon Feb 14, 2023
bfb16b2
clean up some TODOs
nornagon Feb 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 12 additions & 3 deletions docs/api/client-request.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ following properties:
with which the request is associated. Defaults to the empty string. The
`session` option supersedes `partition`. Thus if a `session` is explicitly
specified, `partition` is ignored.
* `credentials` string (optional) - Can be `include` or `omit`. Whether to
send [credentials](https://fetch.spec.whatwg.org/#credentials) with this
* `credentials` string (optional) - Can be `include`, `omit` or
`same-origin`. Whether to send
[credentials](https://fetch.spec.whatwg.org/#credentials) with this
request. If set to `include`, credentials from the session associated with
the request will be used. If set to `omit`, credentials will not be sent
with the request (and the `'login'` event will not be triggered in the
event of a 401). This matches the behavior of the
event of a 401). If set to `same-origin`, `origin` must also be specified.
This matches the behavior of the
[fetch](https://fetch.spec.whatwg.org/#concept-request-credentials-mode)
option of the same name. If this option is not specified, authentication
data from the session will be sent, and cookies will not be sent (unless
Expand All @@ -49,6 +51,13 @@ following properties:
[`request.followRedirect`](#requestfollowredirect) is invoked synchronously
during the [`redirect`](#event-redirect) event. Defaults to `follow`.
* `origin` string (optional) - The origin URL of the request.
* `referrerPolicy` string (optional) - can be `""`, `no-referrer`,
nornagon marked this conversation as resolved.
Show resolved Hide resolved
`no-referrer-when-downgrade`, `origin`, `origin-when-cross-origin`,
`unsafe-url`, `same-origin`, `strict-origin`, or
`strict-origin-when-cross-origin`. Defaults to
`strict-origin-when-cross-origin`.
* `cache` string (optional) - can be `""`, `default`, `no-store`, `reload`,
nornagon marked this conversation as resolved.
Show resolved Hide resolved
`no-cache`, `force-cache` or `only-if-cached`.

`options` properties such as `protocol`, `host`, `hostname`, `port` and `path`
strictly follow the Node.js model as described in the
Expand Down
22 changes: 22 additions & 0 deletions docs/api/net.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,28 @@ Creates a [`ClientRequest`](./client-request.md) instance using the provided
The `net.request` method would be used to issue both secure and insecure HTTP
requests according to the specified protocol scheme in the `options` object.

### `net.fetch(input[, init])`

* `input` RequestInfo
* `init` RequestInit (optional)
nornagon marked this conversation as resolved.
Show resolved Hide resolved

Returns `Promise<Response>`.

Sends a request, similarly to how `fetch()` works in the renderer, using
Chrome's network stack. This differs from Node's `fetch()`, which uses an
entirely separate HTTP stack, with different limitations.
Copy link
Member

Choose a reason for hiding this comment

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

I think it'd be useful to include a short explainer on why this might be useful. I have a few ideas, but I imagine many readers would not be sure when to use this.

Copy link
Member Author

Choose a reason for hiding this comment

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

That's a good idea. I'm not sure exactly what I'd suggest though, other than "this is a nicer API than net.request" :P

My main impetus for adding this is because it fits conveniently into later plans I have for protocol.handle. What do you think some good things to suggest here would be?

Copy link
Member

Choose a reason for hiding this comment

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

Using Chrome's network stack can be better for handling network proxies, right? Maybe something about that. Also, would it allow requests made using this API to be intercepted with webRequest?

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, you mean to contrast it with Node's net stack, rather than to contrast it with net.request. That makes sense, though since net.fetch and net.request use the same network stack, I think such a distinction belongs at the top of net.md, as opposed to here in the fetch documentation (though we should link it here).

Since that would be additional documentation for both net.request and net.fetch, I'd like to do that in a followup PR.


See the MDN documentation for
[`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/fetch) for more
details.

Limitations:

* `net.fetch()` does not support the `data:` or `blob:` schemes.
* The value of the `integrity` option is ignored.
* The `.type` and `.url` values of the returned `Response` object are
incorrect.

### `net.isOnline()`

Returns `boolean` - Whether there is currently internet connection.
Expand Down
120 changes: 119 additions & 1 deletion lib/browser/api/net.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,9 @@ function parseOptions (optionsIn: ClientRequestConstructorOptions | string): Nod
body: null as any,
useSessionCookies: options.useSessionCookies,
credentials: options.credentials,
origin: options.origin
origin: options.origin,
referrerPolicy: options.referrerPolicy,
cache: options.cache
};
const headers: Record<string, string | string[]> = options.headers || {};
for (const [name, value] of Object.entries(headers)) {
Expand Down Expand Up @@ -310,6 +312,7 @@ export class ClientRequest extends Writable implements Electron.ClientRequest {
}

const { redirectPolicy, ...urlLoaderOptions } = parseOptions(options);
if (urlLoaderOptions.credentials === 'same-origin' && !urlLoaderOptions.origin) { throw new Error('credentials: same-origin requires origin to be set'); }
this._urlLoaderOptions = urlLoaderOptions;
this._redirectPolicy = redirectPolicy;
}
Expand Down Expand Up @@ -526,6 +529,121 @@ export function request (options: ClientRequestConstructorOptions | string, call
return new ClientRequest(options, callback);
}

function createDeferredPromise<T, E extends Error = Error> (): { promise: Promise<T>; resolve: (x: T) => void; reject: (e: E) => void; } {
let res: (x: T) => void;
let rej: (e: E) => void;
const promise = new Promise<T>((resolve, reject) => {
res = resolve;
rej = reject;
});

return { promise, resolve: res!, reject: rej! };
}

export function fetch (input: RequestInfo, init?: RequestInit): Promise<Response> {
Copy link
Member

Choose a reason for hiding this comment

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

Are the RequestInfo and RequestInit types coming from Node.js ?

Copy link
Member Author

Choose a reason for hiding this comment

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

yep.

Copy link
Member Author

Choose a reason for hiding this comment

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

actually, it looks like these types are coming from lib.dom.d.ts in Typescript. @types/node doesn't export them, though undici does define them: https://github.com/nodejs/undici/blob/2b260c997ad4efe4ed2064b264b4b546a59e7a67/types/fetch.d.ts#L103

This turns out to cause some minor issues (e.g. TypeScript getting confused between the Web's ReadableStream and Node's Web-compatible ReadableStream) but on the whole the types should be pretty similar, so not too big a deal I think, at least until Node sorts out what it's doing w.r.t. fetch &c.

const p = createDeferredPromise<Response>();
let req: Request;
try {
req = new Request(input, init);
nornagon marked this conversation as resolved.
Show resolved Hide resolved
} catch (e: any) {
p.reject(e);
return p.promise;
}

if (req.signal.aborted) {
// 1. Abort the fetch() call with p, request, null, and
// requestObject’s signal’s abort reason.
const error = (req.signal as any).reason ?? new DOMException('The operation was aborted.', 'AbortError');
p.reject(error);

if (req.body != null /* && isReadable(req.body) */) {
nornagon marked this conversation as resolved.
Show resolved Hide resolved
req.body.cancel(error).catch((err) => {
if (err.code === 'ERR_INVALID_STATE') {
// Node bug?
return;
nornagon marked this conversation as resolved.
Show resolved Hide resolved
}
throw err;
});
}

// 2. Return p.
return p.promise;
}

let locallyAborted = false;
req.signal.addEventListener(
'abort',
() => {
// 1. Set locallyAborted to true.
locallyAborted = true;

// 2. Abort the fetch() call with p, request, responseObject,
// and requestObject’s signal’s abort reason.
const error = (req.signal as any).reason ?? new DOMException('The operation was aborted.', 'AbortError');
p.reject(error);
if (req.body != null /* && isReadable(req.body) */) {
req.body.cancel(error).catch((err) => {
if (err.code === 'ERR_INVALID_STATE') {
// Node bug?
return;
}
throw err;
});
}

r.abort();
},
{ once: true }
);

const origin = req.headers.get('origin') ?? undefined;
// We can't set credentials to same-origin unless there's an origin set.
const credentials = req.credentials === 'same-origin' && !origin ? 'include' : req.credentials;

const r = request({
// TODO: session
nornagon marked this conversation as resolved.
Show resolved Hide resolved
method: req.method,
url: req.url,
origin,
credentials,
cache: req.cache,
referrerPolicy: req.referrerPolicy,
redirect: req.redirect
});

if (req.mode) {
r.setHeader('Sec-Fetch-Mode', req.mode);
}

for (const [k, v] of req.headers) {
r.setHeader(k, v);
}

r.on('response', (resp: IncomingMessage) => {
if (locallyAborted) return;
const headers = new Headers();
for (const [k, v] of Object.entries(resp.headers)) { headers.set(k, Array.isArray(v) ? v.join(', ') : v); }
// TODO: this loses trailers and httpVersion info
const nullBodyStatus = [101, 204, 205, 304];
const body = nullBodyStatus.includes(resp.statusCode) || req.method === 'HEAD' ? null : Readable.toWeb(resp) as ReadableStream;
const rResp = new Response(body, {
headers,
status: resp.statusCode,
statusText: resp.statusMessage
});
p.resolve(rResp);
});

r.on('error', (err) => {
// TODO: abort..?
p.reject(err);
});

if (!req.body?.pipeTo(Writable.toWeb(r)).then(() => r.end())) { r.end(); }

return p.promise;
}

exports.isOnline = isOnline;

Object.defineProperty(exports, 'online', {
Expand Down
110 changes: 107 additions & 3 deletions shell/browser/api/electron_api_url_loader.cc
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/object_template_builder.h"
#include "shell/common/node_includes.h"
#include "third_party/blink/public/common/loader/referrer_utils.h"
#include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom.h"

namespace gin {

Expand Down Expand Up @@ -59,15 +61,84 @@ struct Converter<network::mojom::CredentialsMode> {
*out = network::mojom::CredentialsMode::kOmit;
else if (mode == "include")
*out = network::mojom::CredentialsMode::kInclude;
else if (mode == "same-origin")
// Note: This only makes sense if the request specifies the "origin"
// option.
*out = network::mojom::CredentialsMode::kSameOrigin;
else
// "same-origin" is technically a member of this enum as well, but it
// doesn't make sense in the context of `net.request()`, so don't convert
// it.
return false;
return true;
}
};

template <>
struct Converter<blink::mojom::FetchCacheMode> {
static bool FromV8(v8::Isolate* isolate,
v8::Local<v8::Value> val,
blink::mojom::FetchCacheMode* out) {
std::string cache;
if (!ConvertFromV8(isolate, val, &cache))
return false;
if (cache == "default" || cache == "") {
*out = blink::mojom::FetchCacheMode::kDefault;
} else if (cache == "no-store") {
*out = blink::mojom::FetchCacheMode::kNoStore;
} else if (cache == "reload") {
*out = blink::mojom::FetchCacheMode::kBypassCache;
} else if (cache == "no-cache") {
*out = blink::mojom::FetchCacheMode::kValidateCache;
} else if (cache == "force-cache") {
*out = blink::mojom::FetchCacheMode::kForceCache;
} else if (cache == "only-if-cached") {
*out = blink::mojom::FetchCacheMode::kOnlyIfCached;
} else {
return false;
}
return true;
}
};

template <>
struct Converter<net::ReferrerPolicy> {
static bool FromV8(v8::Isolate* isolate,
v8::Local<v8::Value> val,
net::ReferrerPolicy* out) {
std::string referrer_policy;
if (!ConvertFromV8(isolate, val, &referrer_policy))
return false;
if (base::CompareCaseInsensitiveASCII(referrer_policy, "no-referrer") ==
0) {
*out = net::ReferrerPolicy::NO_REFERRER;
} else if (base::CompareCaseInsensitiveASCII(
referrer_policy, "no-referrer-when-downgrade") == 0) {
*out = net::ReferrerPolicy::CLEAR_ON_TRANSITION_FROM_SECURE_TO_INSECURE;
} else if (base::CompareCaseInsensitiveASCII(referrer_policy, "origin") ==
0) {
*out = net::ReferrerPolicy::ORIGIN;
} else if (base::CompareCaseInsensitiveASCII(
referrer_policy, "origin-when-cross-origin") == 0) {
*out = net::ReferrerPolicy::ORIGIN_ONLY_ON_TRANSITION_CROSS_ORIGIN;
} else if (base::CompareCaseInsensitiveASCII(referrer_policy,
"unsafe-url") == 0) {
*out = net::ReferrerPolicy::NEVER_CLEAR;
} else if (base::CompareCaseInsensitiveASCII(referrer_policy,
"same-origin") == 0) {
*out = net::ReferrerPolicy::CLEAR_ON_TRANSITION_CROSS_ORIGIN;
} else if (base::CompareCaseInsensitiveASCII(referrer_policy,
"strict-origin") == 0) {
*out = net::ReferrerPolicy::
ORIGIN_CLEAR_ON_TRANSITION_FROM_SECURE_TO_INSECURE;
} else if (referrer_policy == "" ||
base::CompareCaseInsensitiveASCII(
referrer_policy, "strict-origin-when-cross-origin") == 0) {
*out = net::ReferrerPolicy::REDUCE_GRANULARITY_ON_TRANSITION_CROSS_ORIGIN;
} else {
return false;
}
return true;
}
};

} // namespace gin

namespace electron::api {
Expand Down Expand Up @@ -401,6 +472,9 @@ gin::Handle<SimpleURLLoaderWrapper> SimpleURLLoaderWrapper::Create(
opts.Get("url", &request->url);
request->site_for_cookies = net::SiteForCookies::FromUrl(request->url);
opts.Get("referrer", &request->referrer);
request->referrer_policy =
blink::ReferrerUtils::GetDefaultNetReferrerPolicy();
opts.Get("referrerPolicy", &request->referrer_policy);
std::string origin;
opts.Get("origin", &origin);
if (!origin.empty()) {
Expand Down Expand Up @@ -484,6 +558,36 @@ gin::Handle<SimpleURLLoaderWrapper> SimpleURLLoaderWrapper::Create(
}
}

blink::mojom::FetchCacheMode cache_mode =
blink::mojom::FetchCacheMode::kDefault;
opts.Get("cache", &cache_mode);
switch (cache_mode) {
case blink::mojom::FetchCacheMode::kNoStore:
request->load_flags |= net::LOAD_DISABLE_CACHE;
break;
case blink::mojom::FetchCacheMode::kValidateCache:
request->load_flags |= net::LOAD_VALIDATE_CACHE;
break;
case blink::mojom::FetchCacheMode::kBypassCache:
request->load_flags |= net::LOAD_BYPASS_CACHE;
break;
case blink::mojom::FetchCacheMode::kForceCache:
request->load_flags |= net::LOAD_SKIP_CACHE_VALIDATION;
break;
case blink::mojom::FetchCacheMode::kOnlyIfCached:
request->load_flags |=
net::LOAD_ONLY_FROM_CACHE | net::LOAD_SKIP_CACHE_VALIDATION;
break;
case blink::mojom::FetchCacheMode::kUnspecifiedOnlyIfCachedStrict:
request->load_flags |= net::LOAD_ONLY_FROM_CACHE;
break;
case blink::mojom::FetchCacheMode::kDefault:
break;
case blink::mojom::FetchCacheMode::kUnspecifiedForceCacheMiss:
request->load_flags |= net::LOAD_ONLY_FROM_CACHE | net::LOAD_BYPASS_CACHE;
break;
}

bool use_session_cookies = false;
opts.Get("useSessionCookies", &use_session_cookies);
int options = 0;
Expand Down
4 changes: 3 additions & 1 deletion typings/internal-ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,13 @@ declare namespace NodeJS {
url: string;
extraHeaders?: Record<string, string>;
useSessionCookies?: boolean;
credentials?: 'include' | 'omit';
credentials?: 'include' | 'omit' | 'same-origin';
body: Uint8Array | BodyFunc;
session?: Electron.Session;
partition?: string;
referrer?: string;
referrerPolicy?: string;
cache?: string;
origin?: string;
hasUserActivation?: boolean;
mode?: string;
Expand Down