2019-01-21 14:03:30 -05:00
|
|
|
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
|
2019-11-02 11:47:55 -04:00
|
|
|
import { createResolvable, notImplemented, isTypedArray } from "./util.ts";
|
|
|
|
import * as body from "./body.ts";
|
2019-09-02 17:07:11 -04:00
|
|
|
import * as domTypes from "./dom_types.ts";
|
2019-11-02 11:47:55 -04:00
|
|
|
import { TextEncoder } from "./text_encoding.ts";
|
2019-09-02 17:07:11 -04:00
|
|
|
import { DenoBlob, bytesSymbol as blobBytesSymbol } from "./blob.ts";
|
|
|
|
import { Headers } from "./headers.ts";
|
2019-11-02 11:47:55 -04:00
|
|
|
import { EOF } from "./io.ts";
|
2019-09-02 17:07:11 -04:00
|
|
|
import { read, close } from "./files.ts";
|
|
|
|
import { URLSearchParams } from "./url_search_params.ts";
|
|
|
|
import * as dispatch from "./dispatch.ts";
|
|
|
|
import { sendAsync } from "./dispatch_json.ts";
|
2019-11-02 11:47:55 -04:00
|
|
|
import { ReadableStream } from "./streams/mod.ts";
|
2018-12-21 17:09:53 -05:00
|
|
|
|
2019-11-02 11:47:55 -04:00
|
|
|
interface ReadableStreamController {
|
|
|
|
enqueue(chunk: string | ArrayBuffer): void;
|
|
|
|
close(): void;
|
2018-12-21 17:09:53 -05:00
|
|
|
}
|
|
|
|
|
2019-11-02 11:47:55 -04:00
|
|
|
class UnderlyingRIDSource implements domTypes.UnderlyingSource {
|
|
|
|
constructor(private rid: number) {
|
|
|
|
this.rid = rid;
|
2018-10-26 12:14:06 -04:00
|
|
|
}
|
|
|
|
|
2019-11-02 11:47:55 -04:00
|
|
|
start(controller: ReadableStreamController): Promise<void> {
|
|
|
|
const buff: Uint8Array = new Uint8Array(32 * 1024);
|
|
|
|
const pump = (): Promise<void> => {
|
|
|
|
return read(this.rid, buff).then(value => {
|
|
|
|
if (value == EOF) {
|
|
|
|
close(this.rid);
|
|
|
|
return controller.close();
|
2018-12-21 17:09:53 -05:00
|
|
|
}
|
2019-11-02 11:47:55 -04:00
|
|
|
controller.enqueue(buff.slice(0, value));
|
|
|
|
return pump();
|
|
|
|
});
|
|
|
|
};
|
|
|
|
return pump();
|
2018-10-26 12:14:06 -04:00
|
|
|
}
|
|
|
|
|
2019-11-02 11:47:55 -04:00
|
|
|
cancel(controller: ReadableStreamController): void {
|
2018-10-26 12:14:06 -04:00
|
|
|
close(this.rid);
|
2019-11-02 11:47:55 -04:00
|
|
|
return controller.close();
|
2018-10-26 12:14:06 -04:00
|
|
|
}
|
2019-11-02 11:47:55 -04:00
|
|
|
}
|
2018-10-26 12:14:06 -04:00
|
|
|
|
2019-11-02 11:47:55 -04:00
|
|
|
class Body extends body.Body implements domTypes.ReadableStream {
|
2018-10-26 12:14:06 -04:00
|
|
|
async cancel(): Promise<void> {
|
2019-11-02 11:47:55 -04:00
|
|
|
if (this._stream) {
|
|
|
|
return this._stream.cancel();
|
|
|
|
}
|
|
|
|
throw new Error("no stream present");
|
2018-10-26 12:14:06 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
getReader(): domTypes.ReadableStreamReader {
|
2019-11-02 11:47:55 -04:00
|
|
|
if (this._stream) {
|
|
|
|
return this._stream.getReader();
|
|
|
|
}
|
|
|
|
throw new Error("no stream present");
|
|
|
|
}
|
|
|
|
|
|
|
|
get locked(): boolean {
|
|
|
|
if (this._stream) {
|
|
|
|
return this._stream.locked;
|
|
|
|
}
|
|
|
|
throw new Error("no stream present");
|
2018-10-26 12:14:06 -04:00
|
|
|
}
|
2019-05-01 23:56:42 -04:00
|
|
|
|
|
|
|
tee(): [domTypes.ReadableStream, domTypes.ReadableStream] {
|
2019-11-02 11:47:55 -04:00
|
|
|
if (this._stream) {
|
|
|
|
const streams = this._stream.tee();
|
|
|
|
return [streams[0], streams[1]];
|
|
|
|
}
|
|
|
|
throw new Error("no stream present");
|
2019-05-01 23:56:42 -04:00
|
|
|
}
|
2019-06-22 10:22:27 -04:00
|
|
|
|
|
|
|
[Symbol.asyncIterator](): AsyncIterableIterator<Uint8Array> {
|
2019-11-02 11:47:55 -04:00
|
|
|
//@ts-ignore
|
|
|
|
const reader = this.body.getReader();
|
2019-09-07 12:20:30 -04:00
|
|
|
|
2019-11-02 11:47:55 -04:00
|
|
|
return {
|
|
|
|
[Symbol.asyncIterator](): AsyncIterableIterator<Uint8Array> {
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
|
|
|
async next() {
|
|
|
|
return reader.read();
|
|
|
|
},
|
|
|
|
|
|
|
|
return() {
|
|
|
|
return reader.releaseLock();
|
|
|
|
}
|
|
|
|
} as AsyncIterableIterator<Uint8Array>;
|
2019-09-07 12:20:30 -04:00
|
|
|
}
|
2018-10-26 12:14:06 -04:00
|
|
|
}
|
|
|
|
|
2019-06-13 22:53:30 -04:00
|
|
|
export class Response implements domTypes.Response {
|
2018-07-06 11:20:35 -04:00
|
|
|
readonly type = "basic"; // TODO
|
2019-06-24 09:34:09 -04:00
|
|
|
readonly redirected: boolean;
|
2018-10-23 07:43:43 -04:00
|
|
|
headers: domTypes.Headers;
|
2018-10-07 19:33:30 -04:00
|
|
|
readonly trailer: Promise<domTypes.Headers>;
|
2019-11-02 11:47:55 -04:00
|
|
|
protected _body: Body;
|
2018-07-06 11:20:35 -04:00
|
|
|
|
2018-09-12 15:16:42 -04:00
|
|
|
constructor(
|
2019-08-16 18:20:04 -04:00
|
|
|
readonly url: string,
|
2018-09-12 15:16:42 -04:00
|
|
|
readonly status: number,
|
2019-09-02 12:30:14 -04:00
|
|
|
readonly statusText: string,
|
2018-10-26 12:14:06 -04:00
|
|
|
headersList: Array<[string, string]>,
|
|
|
|
rid: number,
|
2019-06-24 09:34:09 -04:00
|
|
|
redirected_: boolean,
|
2019-11-02 11:47:55 -04:00
|
|
|
readableStream_: domTypes.ReadableStream | null = null
|
2018-09-12 15:16:42 -04:00
|
|
|
) {
|
2018-08-15 20:57:36 -04:00
|
|
|
this.trailer = createResolvable();
|
2018-10-23 07:43:43 -04:00
|
|
|
this.headers = new Headers(headersList);
|
2018-10-26 12:14:06 -04:00
|
|
|
const contentType = this.headers.get("content-type") || "";
|
|
|
|
|
2019-11-02 11:47:55 -04:00
|
|
|
if (readableStream_ == null) {
|
|
|
|
const underlyingSource = new UnderlyingRIDSource(rid);
|
|
|
|
const rs = new ReadableStream(underlyingSource);
|
|
|
|
this._body = new Body(rs, contentType);
|
2018-10-26 12:14:06 -04:00
|
|
|
} else {
|
2019-11-02 11:47:55 -04:00
|
|
|
this._body = new Body(readableStream_, contentType);
|
2018-10-26 12:14:06 -04:00
|
|
|
}
|
2019-06-24 09:34:09 -04:00
|
|
|
|
|
|
|
this.redirected = redirected_;
|
2018-07-06 11:20:35 -04:00
|
|
|
}
|
|
|
|
|
2019-11-02 11:47:55 -04:00
|
|
|
get body(): domTypes.ReadableStream | null {
|
|
|
|
return this._body;
|
|
|
|
}
|
|
|
|
|
2018-10-26 12:14:06 -04:00
|
|
|
async arrayBuffer(): Promise<ArrayBuffer> {
|
2019-11-02 11:47:55 -04:00
|
|
|
return this._body.arrayBuffer();
|
2018-07-06 11:20:35 -04:00
|
|
|
}
|
|
|
|
|
2018-10-07 19:33:30 -04:00
|
|
|
async blob(): Promise<domTypes.Blob> {
|
2019-11-02 11:47:55 -04:00
|
|
|
return this._body.blob().then(blob => {
|
|
|
|
return blob;
|
|
|
|
});
|
2018-07-06 11:20:35 -04:00
|
|
|
}
|
|
|
|
|
2018-10-07 19:33:30 -04:00
|
|
|
async formData(): Promise<domTypes.FormData> {
|
2019-11-02 11:47:55 -04:00
|
|
|
return this._body.formData();
|
2018-07-06 11:20:35 -04:00
|
|
|
}
|
|
|
|
|
2019-03-09 12:30:38 -05:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2018-10-26 12:14:06 -04:00
|
|
|
async json(): Promise<any> {
|
2019-11-02 11:47:55 -04:00
|
|
|
return this._body.json();
|
2018-07-06 11:20:35 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
async text(): Promise<string> {
|
2019-11-02 11:47:55 -04:00
|
|
|
return this._body.text();
|
2018-07-06 11:20:35 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
get ok(): boolean {
|
|
|
|
return 200 <= this.status && this.status < 300;
|
|
|
|
}
|
|
|
|
|
2019-09-07 12:20:30 -04:00
|
|
|
get bodyUsed(): boolean {
|
2019-11-02 11:47:55 -04:00
|
|
|
return this._body.bodyUsed;
|
2019-09-07 12:20:30 -04:00
|
|
|
}
|
|
|
|
|
2018-10-07 19:33:30 -04:00
|
|
|
clone(): domTypes.Response {
|
2018-10-21 17:42:18 -04:00
|
|
|
if (this.bodyUsed) {
|
|
|
|
throw new TypeError(
|
|
|
|
"Failed to execute 'clone' on 'Response': Response body is already used"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const iterators = this.headers.entries();
|
|
|
|
const headersList: Array<[string, string]> = [];
|
|
|
|
for (const header of iterators) {
|
|
|
|
headersList.push(header);
|
|
|
|
}
|
|
|
|
|
2019-11-02 11:47:55 -04:00
|
|
|
let clonedStream: domTypes.ReadableStream | null = null;
|
|
|
|
if (this._body.body) {
|
|
|
|
const streams = this._body.body.tee();
|
|
|
|
clonedStream = streams[1];
|
|
|
|
this._body = new Body(streams[0], this._body.contentType);
|
|
|
|
}
|
|
|
|
|
2019-06-24 09:34:09 -04:00
|
|
|
return new Response(
|
2019-08-16 18:20:04 -04:00
|
|
|
this.url,
|
2019-06-24 09:34:09 -04:00
|
|
|
this.status,
|
2019-09-02 12:30:14 -04:00
|
|
|
this.statusText,
|
2019-06-24 09:34:09 -04:00
|
|
|
headersList,
|
|
|
|
-1,
|
|
|
|
this.redirected,
|
2019-11-02 11:47:55 -04:00
|
|
|
clonedStream
|
2019-06-24 09:34:09 -04:00
|
|
|
);
|
2018-07-06 11:20:35 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-26 08:50:21 -04:00
|
|
|
interface FetchResponse {
|
|
|
|
bodyRid: number;
|
|
|
|
status: number;
|
2019-09-02 12:30:14 -04:00
|
|
|
statusText: string;
|
2019-08-26 08:50:21 -04:00
|
|
|
headers: Array<[string, string]>;
|
2019-08-24 16:20:48 -04:00
|
|
|
}
|
2019-08-24 09:02:42 -04:00
|
|
|
|
2019-08-26 08:50:21 -04:00
|
|
|
async function sendFetchReq(
|
2019-08-24 16:20:48 -04:00
|
|
|
url: string,
|
|
|
|
method: string | null,
|
|
|
|
headers: domTypes.Headers | null,
|
|
|
|
body: ArrayBufferView | undefined
|
2019-08-26 08:50:21 -04:00
|
|
|
): Promise<FetchResponse> {
|
|
|
|
let headerArray: Array<[string, string]> = [];
|
|
|
|
if (headers) {
|
|
|
|
headerArray = Array.from(headers.entries());
|
|
|
|
}
|
|
|
|
|
|
|
|
let zeroCopy = undefined;
|
|
|
|
if (body) {
|
|
|
|
zeroCopy = new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
|
|
|
|
}
|
|
|
|
|
|
|
|
const args = {
|
|
|
|
method,
|
|
|
|
url,
|
|
|
|
headers: headerArray
|
|
|
|
};
|
|
|
|
|
|
|
|
return (await sendAsync(dispatch.OP_FETCH, args, zeroCopy)) as FetchResponse;
|
2019-06-24 09:34:09 -04:00
|
|
|
}
|
|
|
|
|
2018-10-14 16:29:50 -04:00
|
|
|
/** Fetch a resource from the network. */
|
2018-09-05 22:13:36 -04:00
|
|
|
export async function fetch(
|
2018-11-14 20:36:34 -05:00
|
|
|
input: domTypes.Request | string,
|
2018-10-07 19:33:30 -04:00
|
|
|
init?: domTypes.RequestInit
|
2018-10-26 12:14:06 -04:00
|
|
|
): Promise<Response> {
|
2018-11-14 20:36:34 -05:00
|
|
|
let url: string;
|
|
|
|
let method: string | null = null;
|
|
|
|
let headers: domTypes.Headers | null = null;
|
2018-11-14 21:19:38 -05:00
|
|
|
let body: ArrayBufferView | undefined;
|
2019-06-24 09:34:09 -04:00
|
|
|
let redirected = false;
|
|
|
|
let remRedirectCount = 20; // TODO: use a better way to handle
|
2018-11-14 20:36:34 -05:00
|
|
|
|
|
|
|
if (typeof input === "string") {
|
|
|
|
url = input;
|
|
|
|
if (init != null) {
|
|
|
|
method = init.method || null;
|
|
|
|
if (init.headers) {
|
|
|
|
headers =
|
|
|
|
init.headers instanceof Headers
|
|
|
|
? init.headers
|
|
|
|
: new Headers(init.headers);
|
|
|
|
} else {
|
|
|
|
headers = null;
|
|
|
|
}
|
2018-11-14 21:19:38 -05:00
|
|
|
|
2019-01-03 06:41:20 -05:00
|
|
|
// ref: https://fetch.spec.whatwg.org/#body-mixin
|
|
|
|
// Body should have been a mixin
|
|
|
|
// but we are treating it as a separate class
|
2018-11-14 21:19:38 -05:00
|
|
|
if (init.body) {
|
2019-01-03 06:41:20 -05:00
|
|
|
if (!headers) {
|
|
|
|
headers = new Headers();
|
|
|
|
}
|
|
|
|
let contentType = "";
|
2018-11-14 21:19:38 -05:00
|
|
|
if (typeof init.body === "string") {
|
|
|
|
body = new TextEncoder().encode(init.body);
|
2019-01-03 06:41:20 -05:00
|
|
|
contentType = "text/plain;charset=UTF-8";
|
2018-11-14 21:19:38 -05:00
|
|
|
} else if (isTypedArray(init.body)) {
|
|
|
|
body = init.body;
|
2019-01-03 06:41:20 -05:00
|
|
|
} else if (init.body instanceof URLSearchParams) {
|
|
|
|
body = new TextEncoder().encode(init.body.toString());
|
|
|
|
contentType = "application/x-www-form-urlencoded;charset=UTF-8";
|
|
|
|
} else if (init.body instanceof DenoBlob) {
|
|
|
|
body = init.body[blobBytesSymbol];
|
|
|
|
contentType = init.body.type;
|
2018-11-14 21:19:38 -05:00
|
|
|
} else {
|
2019-01-03 06:41:20 -05:00
|
|
|
// TODO: FormData, ReadableStream
|
2018-11-14 21:19:38 -05:00
|
|
|
notImplemented();
|
|
|
|
}
|
2019-01-03 06:41:20 -05:00
|
|
|
if (contentType && !headers.has("content-type")) {
|
|
|
|
headers.set("content-type", contentType);
|
|
|
|
}
|
2018-11-14 21:19:38 -05:00
|
|
|
}
|
2018-11-14 20:36:34 -05:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
url = input.url;
|
|
|
|
method = input.method;
|
|
|
|
headers = input.headers;
|
2019-05-31 17:00:37 -04:00
|
|
|
|
|
|
|
//@ts-ignore
|
|
|
|
if (input._bodySource) {
|
|
|
|
body = new DataView(await input.arrayBuffer());
|
|
|
|
}
|
2018-11-14 20:36:34 -05:00
|
|
|
}
|
2018-09-05 22:13:36 -04:00
|
|
|
|
2019-06-24 09:34:09 -04:00
|
|
|
while (remRedirectCount) {
|
2019-08-26 08:50:21 -04:00
|
|
|
const fetchResponse = await sendFetchReq(url, method, headers, body);
|
2019-06-24 09:34:09 -04:00
|
|
|
|
2019-08-16 18:20:04 -04:00
|
|
|
const response = new Response(
|
|
|
|
url,
|
2019-08-26 08:50:21 -04:00
|
|
|
fetchResponse.status,
|
2019-09-02 12:30:14 -04:00
|
|
|
fetchResponse.statusText,
|
2019-08-26 08:50:21 -04:00
|
|
|
fetchResponse.headers,
|
|
|
|
fetchResponse.bodyRid,
|
2019-08-16 18:20:04 -04:00
|
|
|
redirected
|
|
|
|
);
|
2019-06-24 09:34:09 -04:00
|
|
|
if ([301, 302, 303, 307, 308].includes(response.status)) {
|
|
|
|
// We're in a redirect status
|
|
|
|
switch ((init && init.redirect) || "follow") {
|
|
|
|
case "error":
|
|
|
|
throw notImplemented();
|
|
|
|
case "manual":
|
|
|
|
throw notImplemented();
|
|
|
|
case "follow":
|
|
|
|
default:
|
|
|
|
let redirectUrl = response.headers.get("Location");
|
|
|
|
if (redirectUrl == null) {
|
|
|
|
return response; // Unspecified
|
|
|
|
}
|
|
|
|
if (
|
|
|
|
!redirectUrl.startsWith("http://") &&
|
|
|
|
!redirectUrl.startsWith("https://")
|
|
|
|
) {
|
|
|
|
redirectUrl =
|
|
|
|
url.split("//")[0] +
|
|
|
|
"//" +
|
|
|
|
url.split("//")[1].split("/")[0] +
|
|
|
|
redirectUrl; // TODO: handle relative redirection more gracefully
|
|
|
|
}
|
|
|
|
url = redirectUrl;
|
|
|
|
redirected = true;
|
|
|
|
remRedirectCount--;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return response;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Return a network error due to too many redirections
|
|
|
|
throw notImplemented();
|
2018-07-06 11:20:35 -04:00
|
|
|
}
|