Issue 1:
Context

Currently, fetch() does not allow file:// schemes to be fetched. The inability to do this becomes a significant usability issue, making code less portable. It has become quite common in Deno to write isomorphic code that uses import.meta.url as a base for accessing a resource when writing server code. Currently a user would have to code path to determine if the resource they are trying to access is local or on the network at a code level.

It was requested in #2150 is the 10th most 👍 open issue at the time of this writing.

That being said, the web platform does not support it, because of the security considerations and undefined behavior. Specifically the Fetch Living Standard says:

For now, unfortunate as it is, file URLs are left as an exercise for the

reader.

When in doubt, return a network error.

The specification does not prohibit file protocols, but the entire behavior is undefined.

And Node.js libraries providing the fetch() API have decided not to implement local file URLs, partly because of the undefined behavior and the security concerns that come with Node.js having a trust-by-default model.

Firefox

Firefox is the only mainstream browser to support local "file://" URLs well and would be the only one considered as setting precedence with local file fetch().

Since Firefox 67, by default, local files create their own unique opaque origin, instead of sharing an origin (ref). This means the only file that can be fetched is the file itself. For example scripts that come from file:///example/test.html can only do fetch("file:///example/test.html") or fetch("./test.html"). Every other one will display Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at in the console and throw a network error.

If you change the config option privacy.file_unique_origin to false, then you can fetch local files from pages that have a local origin. There are the observed behaviours of fetching local files this way:

URLs are relative to the source file/window.location

The window.location.origin is opaque, irrespective of it being unique per file or not.

If the file is present on the file system, the fetch is resolved as a 200.

Irrespective of the method, the response is the same. This means HEAD returns with a body.

There are no headers set on the response (including no content-size).

If the file is not present on the file system, the Cross-Origin Request Blocked error is logged to the console, and a network error is thrown. (I suspect the behaviour around the Cross-Origin Request Blocked is part of the security mitigation to limit the ability of scripts to try to detect if the privacy.file_unique_origin is true or false).

Since you have to do advanced configuration to be able to enable this, it feels like its behaviours shouldn't overly influence, as most code in the wild wouldn't expect to be able to fetch local files.

Solution

Scheme Fetch

Building upon 4.2. Scheme fetch and switch on the current URL's scheme, and running the associated steps:

↪ "file"

Run these steps, but abort when the ongoing fetch is terminated:

If request's method is not GET, then return a network error.

Set result to the result of the resolution of op_open_async with an options argument of path set to the result of pathFromURL() for the url and options set to read to true.

If the result is an error, return a network error.

Otherwise, set file to the an instance of FileStream constructed with the rid from result.

Let response be a new response whose status message is OK.

Set response's body's stream to the file's stream.

Return response.

If aborted, then:

Let aborted be the termination's aborted flag.

If aborted is set, then return an aborted network error.

Return a network error.

Body used

TBC steps to close the FileStream.

FileStream

An internal class which encapsulates a success result of op_open_async and performs op_read_async to read and enqueue chunks.

class FileStream {
constructor(rid: number);

readonly closed: boolean;
readonly rid: number;
readonly readable: ReadableStream<Uint8Array>;

close(): Promise<void>;
}

The readable should have a similar chunk queuing strategy to reading the body of a network request, in that it uses "byte" type and the pull() algorithm which provides back pressure on the stream.

Considerations

Security considerations should be returned as thrown errors form the op_open_async and be surfaced in a meaningful way. Because this builds upon the Fetch specification, any error condition, including security or non-found (for like a blob URL) return network errors.

Relative URLs are not supported. Determining a base is complex and is prone to a lot of issues. If we have an established standard around the root of a program, related to auto-discovery of a project configuration and have an established pattern around an implicit window.location and worker .location, then we could consider it, but using import.meta.url is the only viable near-term solution.

Additional features, like range requests, last modified headers, supporting HEAD requests, etc. could be added in the future. It feels appropriate to simply focus on a minimum-viable solution.

It is best to leave content-type up to the consumer, as it becomes an very opinionated thing to determine content-types based on extensions in the file system.

Unlike blobs, the content-length header is not set, as the implementation reads the file in a streaming fashion, meaning that the length of the content can change as the content is being read. Therefore consumers who need to "forward" the content length will need to calculate the length through some other mechanism. (Also, note that this header is not set on Firefox when fetching local files).

Issue 2:
The global object currently surfaces the following:

> this
{ Deno, denoMain, window, atob, btoa, fetch, clearTimeout, clearInterval,
setTimeout, setInterval, location, Blob, File, CustomEventInit, CustomEvent,
EventInit, Event, EventTarget, URL, URLSearchParams, Headers, FormData,
TextEncoder, TextDecoder, performance, onmessage, workerMain,
workerClose, postMessage, Worker }

Since fetch uses the Request and Response interfaces, and those interfaces are often used along with fetch, those too should be surfaced (they are in Chrome, for example). The absence of these interfaces causes aws4fetch to fail with an Uncaught ReferenceError: Request is not defined error.