Skip to content

Commit

Permalink
feat: add protocol.handle (#36674)
Browse files Browse the repository at this point in the history
  • Loading branch information
nornagon committed Mar 27, 2023
1 parent 6a6908c commit fda8ea9
Show file tree
Hide file tree
Showing 25 changed files with 1,261 additions and 96 deletions.
2 changes: 2 additions & 0 deletions docs/api/client-request.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,8 @@ it is not allowed to add or remove a custom header.
* `encoding` string (optional)
* `callback` Function (optional)

Returns `this`.

Sends the last chunk of the request data. Subsequent write or end operations
will not be allowed. The `finish` event is emitted just after the end operation.

Expand Down
24 changes: 19 additions & 5 deletions docs/api/net.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ requests according to the specified protocol scheme in the `options` object.

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

* `input` string | [Request](https://nodejs.org/api/globals.html#request)
* `init` [RequestInit](https://developer.mozilla.org/en-US/docs/Web/API/fetch#options) (optional)
* `input` string | [GlobalRequest](https://nodejs.org/api/globals.html#request)
* `init` [RequestInit](https://developer.mozilla.org/en-US/docs/Web/API/fetch#options) & { bypassCustomProtocolHandlers?: boolean } (optional)

Returns `Promise<GlobalResponse>` - see [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response).

Expand Down Expand Up @@ -101,9 +101,23 @@ Limitations:
* The `.type` and `.url` values of the returned `Response` object are
incorrect.

Requests made with `net.fetch` can be made to [custom protocols](protocol.md)
as well as `file:`, and will trigger [webRequest](web-request.md) handlers if
present.
By default, requests made with `net.fetch` can be made to [custom
protocols](protocol.md) as well as `file:`, and will trigger
[webRequest](web-request.md) handlers if present. When the non-standard
`bypassCustomProtocolHandlers` option is set in RequestInit, custom protocol
handlers will not be called for this request. This allows forwarding an
intercepted request to the built-in handler. [webRequest](web-request.md)
handlers will still be triggered when bypassing custom protocols.

```js
protocol.handle('https', (req) => {
if (req.url === 'https://my-app.com') {
return new Response('<body>my app</body>')
} else {
return net.fetch(req, { bypassCustomProtocolHandlers: true })
}
})
```

### `net.isOnline()`

Expand Down
112 changes: 88 additions & 24 deletions docs/api/protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,11 @@ An example of implementing a protocol that has the same effect as the
`file://` protocol:

```javascript
const { app, protocol } = require('electron')
const path = require('path')
const url = require('url')
const { app, protocol, net } = require('electron')

app.whenReady().then(() => {
protocol.registerFileProtocol('atom', (request, callback) => {
const filePath = url.fileURLToPath('file://' + request.url.slice('atom://'.length))
callback(filePath)
})
protocol.handle('atom', (request) =>
net.fetch('file://' + request.url.slice('atom://'.length)))
})
```

Expand All @@ -38,14 +34,15 @@ to register it to that session explicitly.
```javascript
const { session, app, protocol } = require('electron')
const path = require('path')
const url = require('url')

app.whenReady().then(() => {
const partition = 'persist:example'
const ses = session.fromPartition(partition)

ses.protocol.registerFileProtocol('atom', (request, callback) => {
const url = request.url.substr(7)
callback({ path: path.normalize(`${__dirname}/${url}`) })
ses.protocol.handle('atom', (request) => {
const path = request.url.slice('atom://'.length)
return net.fetch(url.pathToFileURL(path.join(__dirname, path)))
})

mainWindow = new BrowserWindow({ webPreferences: { partition } })
Expand Down Expand Up @@ -109,7 +106,74 @@ The `<video>` and `<audio>` HTML elements expect protocols to buffer their
responses by default. The `stream` flag configures those elements to correctly
expect streaming responses.

### `protocol.registerFileProtocol(scheme, handler)`
### `protocol.handle(scheme, handler)`

* `scheme` string - scheme to handle, for example `https` or `my-app`. This is
the bit before the `:` in a URL.
* `handler` Function<[GlobalResponse](https://nodejs.org/api/globals.html#response) | Promise<GlobalResponse>>
* `request` [GlobalRequest](https://nodejs.org/api/globals.html#request)

Register a protocol handler for `scheme`. Requests made to URLs with this
scheme will delegate to this handler to determine what response should be sent.

Either a `Response` or a `Promise<Response>` can be returned.

Example:

```js
import { app, protocol } from 'electron'
import { join } from 'path'
import { pathToFileURL } from 'url'

protocol.registerSchemesAsPrivileged([
{
scheme: 'app',
privileges: {
standard: true,
secure: true,
supportsFetchAPI: true
}
}
])

app.whenReady().then(() => {
protocol.handle('app', (req) => {
const { host, pathname } = new URL(req.url)
if (host === 'bundle') {
if (pathname === '/') {
return new Response('<h1>hello, world</h1>', {
headers: { 'content-type': 'text/html' }
})
}
// NB, this does not check for paths that escape the bundle, e.g.
// app://bundle/../../secret_file.txt
return net.fetch(pathToFileURL(join(__dirname, pathname)))
} else if (host === 'api') {
return net.fetch('https://api.my-server.com/' + pathname, {
method: req.method,
headers: req.headers,
body: req.body
})
}
})
})
```

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

### `protocol.unhandle(scheme)`

* `scheme` string - scheme for which to remove the handler.

Removes a protocol handler registered with `protocol.handle`.

### `protocol.isProtocolHandled(scheme)`

* `scheme` string

Returns `boolean` - Whether `scheme` is already handled.

### `protocol.registerFileProtocol(scheme, handler)` _Deprecated_

* `scheme` string
* `handler` Function
Expand All @@ -130,7 +194,7 @@ path or an object that has a `path` property, e.g. `callback(filePath)` or
By default the `scheme` is treated like `http:`, which is parsed differently
from protocols that follow the "generic URI syntax" like `file:`.

### `protocol.registerBufferProtocol(scheme, handler)`
### `protocol.registerBufferProtocol(scheme, handler)` _Deprecated_

* `scheme` string
* `handler` Function
Expand All @@ -154,7 +218,7 @@ protocol.registerBufferProtocol('atom', (request, callback) => {
})
```

### `protocol.registerStringProtocol(scheme, handler)`
### `protocol.registerStringProtocol(scheme, handler)` _Deprecated_

* `scheme` string
* `handler` Function
Expand All @@ -170,7 +234,7 @@ The usage is the same with `registerFileProtocol`, except that the `callback`
should be called with either a `string` or an object that has the `data`
property.

### `protocol.registerHttpProtocol(scheme, handler)`
### `protocol.registerHttpProtocol(scheme, handler)` _Deprecated_

* `scheme` string
* `handler` Function
Expand All @@ -185,7 +249,7 @@ Registers a protocol of `scheme` that will send an HTTP request as a response.
The usage is the same with `registerFileProtocol`, except that the `callback`
should be called with an object that has the `url` property.

### `protocol.registerStreamProtocol(scheme, handler)`
### `protocol.registerStreamProtocol(scheme, handler)` _Deprecated_

* `scheme` string
* `handler` Function
Expand Down Expand Up @@ -234,21 +298,21 @@ protocol.registerStreamProtocol('atom', (request, callback) => {
})
```

### `protocol.unregisterProtocol(scheme)`
### `protocol.unregisterProtocol(scheme)` _Deprecated_

* `scheme` string

Returns `boolean` - Whether the protocol was successfully unregistered

Unregisters the custom protocol of `scheme`.

### `protocol.isProtocolRegistered(scheme)`
### `protocol.isProtocolRegistered(scheme)` _Deprecated_

* `scheme` string

Returns `boolean` - Whether `scheme` is already registered.

### `protocol.interceptFileProtocol(scheme, handler)`
### `protocol.interceptFileProtocol(scheme, handler)` _Deprecated_

* `scheme` string
* `handler` Function
Expand All @@ -261,7 +325,7 @@ Returns `boolean` - Whether the protocol was successfully intercepted
Intercepts `scheme` protocol and uses `handler` as the protocol's new handler
which sends a file as a response.

### `protocol.interceptStringProtocol(scheme, handler)`
### `protocol.interceptStringProtocol(scheme, handler)` _Deprecated_

* `scheme` string
* `handler` Function
Expand All @@ -274,7 +338,7 @@ Returns `boolean` - Whether the protocol was successfully intercepted
Intercepts `scheme` protocol and uses `handler` as the protocol's new handler
which sends a `string` as a response.

### `protocol.interceptBufferProtocol(scheme, handler)`
### `protocol.interceptBufferProtocol(scheme, handler)` _Deprecated_

* `scheme` string
* `handler` Function
Expand All @@ -287,7 +351,7 @@ Returns `boolean` - Whether the protocol was successfully intercepted
Intercepts `scheme` protocol and uses `handler` as the protocol's new handler
which sends a `Buffer` as a response.

### `protocol.interceptHttpProtocol(scheme, handler)`
### `protocol.interceptHttpProtocol(scheme, handler)` _Deprecated_

* `scheme` string
* `handler` Function
Expand All @@ -300,7 +364,7 @@ Returns `boolean` - Whether the protocol was successfully intercepted
Intercepts `scheme` protocol and uses `handler` as the protocol's new handler
which sends a new HTTP request as a response.

### `protocol.interceptStreamProtocol(scheme, handler)`
### `protocol.interceptStreamProtocol(scheme, handler)` _Deprecated_

* `scheme` string
* `handler` Function
Expand All @@ -313,15 +377,15 @@ Returns `boolean` - Whether the protocol was successfully intercepted
Same as `protocol.registerStreamProtocol`, except that it replaces an existing
protocol handler.

### `protocol.uninterceptProtocol(scheme)`
### `protocol.uninterceptProtocol(scheme)` _Deprecated_

* `scheme` string

Returns `boolean` - Whether the protocol was successfully unintercepted

Remove the interceptor installed for `scheme` and restore its original handler.

### `protocol.isProtocolIntercepted(scheme)`
### `protocol.isProtocolIntercepted(scheme)` _Deprecated_

* `scheme` string

Expand Down
20 changes: 17 additions & 3 deletions docs/api/session.md
Original file line number Diff line number Diff line change
Expand Up @@ -784,9 +784,23 @@ Limitations:
* The `.type` and `.url` values of the returned `Response` object are
incorrect.
Requests made with `ses.fetch` can be made to [custom protocols](protocol.md)
as well as `file:`, and will trigger [webRequest](web-request.md) handlers if
present.
By default, requests made with `net.fetch` can be made to [custom
protocols](protocol.md) as well as `file:`, and will trigger
[webRequest](web-request.md) handlers if present. When the non-standard
`bypassCustomProtocolHandlers` option is set in RequestInit, custom protocol
handlers will not be called for this request. This allows forwarding an
intercepted request to the built-in handler. [webRequest](web-request.md)
handlers will still be triggered when bypassing custom protocols.
```js
protocol.handle('https', (req) => {
if (req.url === 'https://my-app.com') {
return new Response('<body>my app</body>')
} else {
return net.fetch(req, { bypassCustomProtocolHandlers: true })
}
})
```
#### `ses.disableNetworkEmulation()`
Expand Down
8 changes: 4 additions & 4 deletions docs/api/structures/upload-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

* `type` 'file' - `file`.
* `filePath` string - Path of file to be uploaded.
* `offset` Integer - Defaults to `0`.
* `length` Integer - Number of bytes to read from `offset`.
* `offset` Integer (optional) - Defaults to `0`.
* `length` Integer (optional) - Number of bytes to read from `offset`.
Defaults to `0`.
* `modificationTime` Double - Last Modification time in
number of seconds since the UNIX epoch.
* `modificationTime` Double (optional) - Last Modification time in
number of seconds since the UNIX epoch. Defaults to `0`.
49 changes: 49 additions & 0 deletions docs/breaking-changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,55 @@ This document uses the following convention to categorize breaking changes:
* **Deprecated:** An API was marked as deprecated. The API will continue to function, but will emit a deprecation warning, and will be removed in a future release.
* **Removed:** An API or feature was removed, and is no longer supported by Electron.

## Planned Breaking API Changes (25.0)

### Deprecated: `protocol.{register,intercept}{Buffer,String,Stream,File,Http}Protocol`

The `protocol.register*Protocol` and `protocol.intercept*Protocol` methods have
been replaced with [`protocol.handle`](api/protocol.md#protocolhandlescheme-handler).

The new method can either register a new protocol or intercept an existing
protocol, and responses can be of any type.

```js
// Deprecated in Electron 25
protocol.registerBufferProtocol('some-protocol', () => {
callback({ mimeType: 'text/html', data: Buffer.from('<h5>Response</h5>') })
})

// Replace with
protocol.handle('some-protocol', () => {
return new Response(
Buffer.from('<h5>Response</h5>'), // Could also be a string or ReadableStream.
{ headers: { 'content-type': 'text/html' } }
)
})
```

```js
// Deprecated in Electron 25
protocol.registerHttpProtocol('some-protocol', () => {
callback({ url: 'https://electronjs.org' })
})

// Replace with
protocol.handle('some-protocol', () => {
return net.fetch('https://electronjs.org')
})
```

```js
// Deprecated in Electron 25
protocol.registerFileProtocol('some-protocol', () => {
callback({ filePath: '/path/to/my/file' })
})

// Replace with
protocol.handle('some-protocol', () => {
return net.fetch('file:///path/to/my/file')
})
```

## Planned Breaking API Changes (24.0)

### API Changed: `nativeImage.createThumbnailFromPath(path, size)`
Expand Down
5 changes: 4 additions & 1 deletion lib/browser/api/net-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function createDeferredPromise<T, E extends Error = Error> (): { promise: Promis
return { promise, resolve: res!, reject: rej! };
}

export function fetchWithSession (input: RequestInfo, init: RequestInit | undefined, session: SessionT): Promise<Response> {
export function fetchWithSession (input: RequestInfo, init: (RequestInit & {bypassCustomProtocolHandlers?: boolean}) | undefined, session: SessionT): Promise<Response> {
const p = createDeferredPromise<Response>();
let req: Request;
try {
Expand Down Expand Up @@ -84,6 +84,8 @@ export function fetchWithSession (input: RequestInfo, init: RequestInit | undefi
redirect: req.redirect
}));

(r as any)._urlLoaderOptions.bypassCustomProtocolHandlers = !!init?.bypassCustomProtocolHandlers;

// cors is the default mode, but we can't set mode=cors without an origin.
if (req.mode && (req.mode !== 'cors' || origin)) {
r.setHeader('Sec-Fetch-Mode', req.mode);
Expand All @@ -104,6 +106,7 @@ export function fetchWithSession (input: RequestInfo, init: RequestInit | undefi
status: resp.statusCode,
statusText: resp.statusMessage
});
(rResp as any).__original_resp = resp;
p.resolve(rResp);
});

Expand Down

0 comments on commit fda8ea9

Please sign in to comment.