From 41c70b154f4dd9adf6df875c653bcf8701511ff0 Mon Sep 17 00:00:00 2001 From: qti3e Date: Wed, 12 Sep 2018 23:46:42 +0430 Subject: [PATCH] Add support for fetch() headers (#727) --- js/deno.ts | 6 +--- js/fetch.ts | 83 +++++++++++++++++++++++++++++++++++++++++---- js/fetch_test.ts | 17 ++++++++++ js/fetch_types.d.ts | 2 +- js/globals.ts | 6 ++++ js/rename.ts | 8 ++--- src/handlers.rs | 33 ++++++++++++++++-- src/msg.fbs | 3 +- 8 files changed, 137 insertions(+), 21 deletions(-) diff --git a/js/deno.ts b/js/deno.ts index 6f71ee4991..187d0f27f7 100644 --- a/js/deno.ts +++ b/js/deno.ts @@ -1,11 +1,7 @@ // Copyright 2018 the Deno authors. All rights reserved. MIT license. // Public deno module. /// -export { - env, - exit, - makeTempDirSync -} from "./os"; +export { env, exit, makeTempDirSync } from "./os"; export { mkdirSync, mkdir } from "./mkdir"; export { removeSync, remove, removeAllSync, removeAll } from "./remove"; export { readFileSync, readFile } from "./read_file"; diff --git a/js/fetch.ts b/js/fetch.ts index e3aa052f92..24905694e7 100644 --- a/js/fetch.ts +++ b/js/fetch.ts @@ -16,19 +16,75 @@ import { Response, Blob, RequestInit, + HeadersInit, FormData } from "./fetch_types"; import { TextDecoder } from "./text_encoding"; -class DenoHeaders implements Headers { - append(name: string, value: string): void { - assert(false, "Implement me"); +interface Header { + name: string; + value: string; +} + +export class DenoHeaders implements Headers { + private readonly headerList: Header[] = []; + + constructor(init?: HeadersInit) { + if (init) { + this._fill(init); + } } + + private _append(header: Header): void { + // TODO(qti3e) Check header based on the fetch spec. + this._appendToHeaderList(header); + } + + private _appendToHeaderList(header: Header): void { + const lowerCaseName = header.name.toLowerCase(); + for (let i = 0; i < this.headerList.length; ++i) { + if (this.headerList[i].name.toLowerCase() === lowerCaseName) { + header.name = this.headerList[i].name; + } + } + this.headerList.push(header); + } + + private _fill(init: HeadersInit): void { + if (Array.isArray(init)) { + for (let i = 0; i < init.length; ++i) { + const header = init[i]; + if (header.length !== 2) { + throw new TypeError("Failed to construct 'Headers': Invalid value"); + } + this._append({ + name: header[0], + value: header[1] + }); + } + } else { + for (const key in init) { + this._append({ + name: key, + value: init[key] + }); + } + } + } + + append(name: string, value: string): void { + this._appendToHeaderList({ name, value }); + } + delete(name: string): void { assert(false, "Implement me"); } get(name: string): string | null { - assert(false, "Implement me"); + for (const header of this.headerList) { + if (header.name.toLowerCase() === name.toLowerCase()) { + return header.value; + } + } return null; } has(name: string): boolean { @@ -54,15 +110,20 @@ class FetchResponse implements Response { statusText = "FIXME"; // TODO readonly type = "basic"; // TODO redirected = false; // TODO - headers = new DenoHeaders(); + headers: DenoHeaders; readonly trailer: Promise; //private bodyChunks: Uint8Array[] = []; private first = true; private bodyWaiter: Resolvable; - constructor(readonly status: number, readonly body_: ArrayBuffer) { + constructor( + readonly status: number, + readonly body_: ArrayBuffer, + headersList: Array<[string, string]> + ) { this.bodyWaiter = createResolvable(); this.trailer = createResolvable(); + this.headers = new DenoHeaders(headersList); setTimeout(() => { this.bodyWaiter.resolve(body_); }, 0); @@ -149,6 +210,14 @@ export async function fetch( assert(bodyArray != null); const body = typedArrayToArrayBuffer(bodyArray!); - const response = new FetchResponse(status, body); + const headersList: Array<[string, string]> = []; + const len = msg.headerKeyLength(); + for (let i = 0; i < len; ++i) { + const key = msg.headerKey(i); + const value = msg.headerValue(i); + headersList.push([key, value]); + } + + const response = new FetchResponse(status, body, headersList); return response; } diff --git a/js/fetch_test.ts b/js/fetch_test.ts index 1af3bc2eb7..2b7f32099f 100644 --- a/js/fetch_test.ts +++ b/js/fetch_test.ts @@ -18,3 +18,20 @@ test(async function fetchPerm() { assertEqual(err.kind, deno.ErrorKind.PermissionDenied); assertEqual(err.name, "PermissionDenied"); }); + +testPerm({ net: true }, async function fetchHeaders() { + const response = await fetch("http://localhost:4545/package.json"); + const headers = response.headers; + assertEqual(headers.get("Content-Type"), "application/json"); + assert(headers.get("Server").startsWith("SimpleHTTP")); +}); + +test(async function headersAppend() { + let err; + try { + const headers = new Headers([["foo", "bar", "baz"]]); + } catch (e) { + err = e; + } + assert(err instanceof TypeError); +}); diff --git a/js/fetch_types.d.ts b/js/fetch_types.d.ts index f00bdd3c58..9d4082c359 100644 --- a/js/fetch_types.d.ts +++ b/js/fetch_types.d.ts @@ -13,7 +13,7 @@ See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. *******************************************************************************/ -type HeadersInit = Headers | string[][] | Record; +type HeadersInit = string[][] | Record; type BodyInit = | Blob | BufferSource diff --git a/js/globals.ts b/js/globals.ts index cd29d34c79..3805f12f57 100644 --- a/js/globals.ts +++ b/js/globals.ts @@ -6,6 +6,7 @@ import * as textEncoding from "./text_encoding"; import * as fetch_ from "./fetch"; import { libdeno } from "./libdeno"; import { globalEval } from "./global-eval"; +import { DenoHeaders } from "./fetch"; declare global { interface Window { @@ -23,6 +24,8 @@ declare global { TextEncoder: typeof TextEncoder; TextDecoder: typeof TextDecoder; + + Headers: typeof Headers; } const clearTimeout: typeof timers.clearTimer; @@ -38,6 +41,7 @@ declare global { // tslint:disable:variable-name const TextEncoder: typeof textEncoding.TextEncoder; const TextDecoder: typeof textEncoding.TextDecoder; + const Headers: typeof DenoHeaders; // tslint:enable:variable-name } @@ -57,3 +61,5 @@ window.TextEncoder = textEncoding.TextEncoder; window.TextDecoder = textEncoding.TextDecoder; window.fetch = fetch_.fetch; + +window.Headers = DenoHeaders; diff --git a/js/rename.ts b/js/rename.ts index 241e274908..5917912f5b 100644 --- a/js/rename.ts +++ b/js/rename.ts @@ -4,8 +4,8 @@ import { flatbuffers } from "flatbuffers"; import * as dispatch from "./dispatch"; /** - * Synchronously renames (moves) oldpath to newpath. If newpath already exists - * and is not a directory, Rename replaces it. OS-specific restrictions may + * Synchronously renames (moves) oldpath to newpath. If newpath already exists + * and is not a directory, Rename replaces it. OS-specific restrictions may * apply when oldpath and newpath are in different directories. * * import { renameSync } from "deno"; @@ -16,8 +16,8 @@ export function renameSync(oldpath: string, newpath: string): void { } /** - * Renames (moves) oldpath to newpath. If newpath already exists - * and is not a directory, Rename replaces it. OS-specific restrictions may + * Renames (moves) oldpath to newpath. If newpath already exists + * and is not a directory, Rename replaces it. OS-specific restrictions may * apply when oldpath and newpath are in different directories. * * import { rename } from "deno"; diff --git a/src/handlers.rs b/src/handlers.rs index 09b4e299a8..326c2f2dd4 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -323,27 +323,54 @@ fn handle_fetch_req(d: *const DenoC, base: &msg::Base) -> Box { let url = url.parse::().unwrap(); let client = Client::new(); - let future = client.get(url).and_then(|res| { + let future = client.get(url).and_then(move |res| { let status = res.status().as_u16() as i32; + + let headers = { + let map = res.headers(); + let keys = map + .keys() + .map(|s| s.as_str().to_string()) + .collect::>(); + let values = map + .values() + .map(|s| s.to_str().unwrap().to_string()) + .collect::>(); + (keys, values) + }; + // TODO Handle streaming body. - res.into_body().concat2().map(move |body| (status, body)) + res + .into_body() + .concat2() + .map(move |body| (status, body, headers)) }); let future = future.map_err(|err| -> DenoError { err.into() }).and_then( - move |(status, body)| { + move |(status, body, headers)| { let builder = &mut FlatBufferBuilder::new(); // Send the first message without a body. This is just to indicate // what status code. let body_off = builder.create_vector(body.as_ref()); + let header_keys: Vec<&str> = headers.0.iter().map(|s| &**s).collect(); + let header_keys_off = + builder.create_vector_of_strings(header_keys.as_slice()); + let header_values: Vec<&str> = headers.1.iter().map(|s| &**s).collect(); + let header_values_off = + builder.create_vector_of_strings(header_values.as_slice()); + let msg = msg::FetchRes::create( builder, &msg::FetchResArgs { id, status, body: Some(body_off), + header_key: Some(header_keys_off), + header_value: Some(header_values_off), ..Default::default() }, ); + Ok(serialize_response( cmd_id, builder, diff --git a/src/msg.fbs b/src/msg.fbs index c630fec9d9..e807132366 100644 --- a/src/msg.fbs +++ b/src/msg.fbs @@ -154,7 +154,8 @@ table FetchReq { table FetchRes { id: uint; status: int; - header_line: [string]; + header_key: [string]; + header_value: [string]; body: [ubyte]; }