Repository: electron
Issue: [Feature Request]: protocol.intercept{Any}Protocol handler ability to call original handler
Labels: enhancement :sparkles:,component/protocol

**Is your feature request related to a problem? Please describe.**
It's possible to intercept schemes (including built-ins like `http:`, `https:`, and `file:`), but doing so requires you to handle all requests for that scheme, there's no way to fall-through to the original handler as far as I can tell. Neglecting to call `callback(...)` simply makes the request hang.

Using `protocol.interceptHttpProtocol('http', ...)` will lead to an infinite loop if you use `callback(request)`. This prevents using the intercepts to do handy things like set a header on all HTTP requests to a certain domain, or blackholing another (useful for preventing tracking, ads, etc):

```javascript
const url = require('url')

protocol.interceptHttpProtocol('https', (request, callback) => {
  const { host, pathname } = url.parse(request.url)

  if (host === 'www.github.com') {
    request.headers['X-Foo'] = 'bar'
  } else if (host === 'www.bitbucket.org') {
    callback({ statusCode: 403 })
    return
  }

  callback(request)  // This will cause an infinite loop
})
```

Being able to selectively handle requests for an intercepted protocol would be very useful. It would provide the ability to use `protocol.interceptFileProtocol('http', ...)` and only intercept certain requests, such as those with the origin `http://localhost/`. This would allow you to simulate local files as being served over HTTP, getting around several of the limitations of the `file://` protocol. Currently there's no easy way to do this as you'll intercept all HTTP requests and lose the ability to talk to the network (at least over HTTP). Even if your handler could retrieve the content, it would have to give it a file path so that the callback can return the path to the file, or you'd need to use the `string`/`buffer`/`stream` variants and open any files yourself and provide the content that way. Neither is very clean. You can also bundle a local web server, but this is overkill most of the time.

I've found this particularly painful for trying to get WebAuthn working without a server in the loop, as Chromium only allows the API for `https` domains, or `http://localhost`, not `file://` (or any other protocol). An intercept seems like a perfect fit for this, except you can't selectively handle that small case.

Selective handling also opens the ability to make protocol handlers in the future act more like middleware which is composable and reusable. If interceptors could be applied like a stack and pass to the next interceptor, you could have libraries which handle things like setting certain headers.

**Describe the solution you'd like**
I think this can be solved with a somewhat minor change to the API. There may be dragons in the implementation, I've glanced at the code which implements the `protocol` module, but I'm not too familiar with it.

My suggested solution would be to add a third argument to the handler function for the `protocol.intercept{Any}Protocol` functions called `next`, and rename the `callback` argument to `done`. `done` would have the same behavior as the current `callback` but the name change makes it more explicit that it will end processing of the request. The new argument, `next`, would fall through to the original handler, the one which would be restored by `protocol.uninterceptProtocol`. `next` would optionally take a request object, allowing you to modify it before passing it along, such as setting a header, or changing the URL allowing redirecting between files.

So, the intercept earlier which currently causes an infinite loop could be written as:

```javascript
const url = require('url')

protocol.interceptHttpProtocol('https', (request, done, next) => {
  const { host, pathname } = url.parse(request.url)

  if (host === 'www.github.com') {
    request.headers['X-Foo'] = 'bar'
  } else if (host === 'www.bitbucket.org') {
    callback({ statusCode: 403 })
    return
  }

  next(request)
})
```

You could also write a handler which pretended to be `http://localhost` by reading files from disk:

```javascript
const url = require('url')

protocol.interceptFileProtocol('http', (request, done, next) => {
  const { host, pathname } = url.parse(request.url)

  if (host === 'localhost') {
    done({ path: path.normalize(`${__dirname}/${pathname}`) })
  } else {
    next()  // Invokes the original 'http:' handler for 'request'
  }
})
```

Or make `file://` check cloud storage if the file isn't found locally:

```javascript
const fs = require('fs')
const url = require('url')

protocol.interceptStreamProtocol('file', (request, done, next) => {
  let { host, pathname } = url.parse(request.url)
  pathname = pathname.slice(1)

  if (host === '') {
    // Absolute path, check if pathname exists, if not
    // check cloud storage (Dropbox, OneDrive, etc)
    if (fs.existsSync(pathname)) {
      next()  // Invoke the original 'file:' handler for 'request'
    } else {
      checkCloud(contents => {
        if (contents) {
          done(/* stream to contents */)
        } else {
          done({ statusCode: 404, data: 'Not Found' })
        }
      })
    }
  } else {
    // Don't allow file:// with a host
    done({ statusCode: 400, data: 'Host Not Allowed' })
  }
})
```

Combined with intercepting a custom protocol:

```javascript
const url = require('url')

protocol.registerFileProtocol('atom', (request, callback) => {
  const { host, pathname } = url.parse(request.url)

  callback({ path: path.normalize(`${__dirname}/${host + pathname}`) })
})

protocol.interceptStreamProtocol('atom', (request, done, next) => {
  const { host, pathname } = url.parse(request.url)

  if (pathname.endsWith('.mp4') || pathname === '/dev/video') {
    done(/* Open a stream to video */)
  } else {
    next()
  }
})
```