From c75ccd6c51fdf3176509e5eb1691d54e998b36f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szikszai=20Guszt=C3=A1v?= Date: Wed, 26 Apr 2023 16:20:47 +0100 Subject: [PATCH 1/3] Refactor HTTP module a bit. - Added response headers to `Http.Response` and `Http.ErrorResponse` - Removed `Http.sendWithId` - The second parameter of `Http.send` now has a default value - The body of the response is now an enum and supports binary values --- core/source/Http.mint | 82 ++++++++++++++++++++++++++++---------- core/tests/tests/Http.mint | 6 +-- 2 files changed, 65 insertions(+), 23 deletions(-) diff --git a/core/source/Http.mint b/core/source/Http.mint index 1ebfb1e1a..ca00a976c 100644 --- a/core/source/Http.mint +++ b/core/source/Http.mint @@ -1,7 +1,7 @@ /* Represents a HTTP header */ record Http.Header { - key : String, - value : String + value : String, + key : String } /* Represents an HTTP request. */ @@ -15,12 +15,23 @@ record Http.Request { /* Represents an HTTP response. */ record Http.Response { - status : Number, - body : String + headers : Map(String, String), + body : Http.ResponseBody, + bodyString : String, + status : Number +} + +/* Represents the body of an HTTP response. */ +enum Http.ResponseBody { + Json(Object) + Html(String) + Text(String) + File(File) } /* Represents an HTTP request which failed to load. */ record Http.ErrorResponse { + headers : Map(String, String), type : Http.Error, status : Number, url : String @@ -239,24 +250,16 @@ module Http { } /* - Sends the request with a generated unique ID. - - "https://httpbin.org/get" - |> Http.get() - |> Http.send() - */ - fun send (request : Http.Request) : Promise(Result(Http.ErrorResponse, Http.Response)) { - sendWithId(request, Uid.generate()) - } - - /* - Sends the request with the given ID so it could be aborted later. + Sends the request with a unique ID (generated by default) so it could be aborted later. "https://httpbin.org/get" |> Http.get() |> Http.sendWithId("my-request") */ - fun sendWithId (request : Http.Request, uid : String) : Promise(Result(Http.ErrorResponse, Http.Response)) { + fun send ( + request : Http.Request, + uid : String = Uid.generate() + ) : Promise(Result(Http.ErrorResponse, Http.Response)) { ` new Promise((resolve, reject) => { if (!this._requests) { this._requests = {} } @@ -266,6 +269,18 @@ module Http { this._requests[#{uid}] = xhr xhr.withCredentials = #{request.withCredentials} + xhr.responseType = "blob" + + const getResponseHeaders = () => { + return xhr + .getAllResponseHeaders() + .trim() + .split(/[\r\n]+/) + .map((line) => { + const parts = line.split(': '); + return [parts.shift(), parts.join(': ')]; + }) + } try { xhr.open(#{request.method}.toUpperCase(), #{request.url}, true) @@ -273,6 +288,7 @@ module Http { delete this._requests[#{uid}] resolve(#{Result::Err({ + headers: `getResponseHeaders()`, type: Http.Error::BadUrl, status: `xhr.status`, url: request.url @@ -287,6 +303,7 @@ module Http { delete this._requests[#{uid}] resolve(#{Result::Err({ + headers: `getResponseHeaders()`, type: Http.Error::NetworkError, status: `xhr.status`, url: request.url @@ -297,18 +314,42 @@ module Http { delete this._requests[#{uid}] resolve(#{Result::Err({ + headers: `getResponseHeaders()`, type: Http.Error::Timeout, status: `xhr.status`, url: request.url })}) }) - xhr.addEventListener('load', (event) => { + xhr.addEventListener('load', async (event) => { delete this._requests[#{uid}] + let header = xhr.getResponseHeader("content-type"); + let responseText = await xhr.response.text(); + let body; + + if (header.startsWith("text/html")) { + body = #{Http.ResponseBody::Html(`responseText`)}; + } else if (header.startsWith("application/json")) { + try { + body = #{Http.ResponseBody::Json(`JSON.parse(responseText)`)}; + } catch (e) { + body = #{Http.ResponseBody::Text(`responseText`)}; + } + } else if (header.startsWith("text/")) { + body = #{Http.ResponseBody::Text(`responseText`)}; + } + + if (!body) { + const parts = #{Url.parse(request.url).path}.split('/'); + body = #{Http.ResponseBody::File(`new File([xhr.response], parts[parts.length - 1], { type: header })`)}; + } + resolve(#{Result::Ok({ - body: `xhr.responseText`, - status: `xhr.status` + headers: `getResponseHeaders()`, + bodyString: `responseText`, + status: `xhr.status`, + body: `body`, })}) }) @@ -316,6 +357,7 @@ module Http { delete this._requests[#{uid}] resolve(#{Result::Err({ + headers: `getResponseHeaders()`, type: Http.Error::Aborted, status: `xhr.status`, url: request.url diff --git a/core/tests/tests/Http.mint b/core/tests/tests/Http.mint index 326450410..f8a3ffa21 100644 --- a/core/tests/tests/Http.mint +++ b/core/tests/tests/Http.mint @@ -232,11 +232,11 @@ suite "Http.hasHeader" { } } -suite "Http.sendWithId" { +suite "Http.send" { test "sends the request with the given ID" { let response = Http.get("/blah") - |> Http.sendWithId("A") + |> Http.send("A") `#{Http.requests()}["A"] != undefined` } @@ -265,7 +265,7 @@ component Test.Http { await Http.empty() |> Http.url(url) |> Http.method(method) - |> Http.sendWithId("test") + |> Http.send("test") |> wrap( ` (async (promise) => { From 0c29599a2f67b9e98c385bb74d767b201f057e41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szikszai=20Guszt=C3=A1v?= Date: Thu, 27 Apr 2023 14:54:37 +0100 Subject: [PATCH 2/3] Parse HTML and XML with `DOMParser` --- core/source/Http.mint | 43 +++++++++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/core/source/Http.mint b/core/source/Http.mint index ca00a976c..3931abb84 100644 --- a/core/source/Http.mint +++ b/core/source/Http.mint @@ -21,11 +21,12 @@ record Http.Response { status : Number } -/* Represents the body of an HTTP response. */ +/* Represents the body of a HTTP response. */ enum Http.ResponseBody { - Json(Object) - Html(String) + JSON(Object) + HTML(Object) Text(String) + XML(Object) File(File) } @@ -324,25 +325,47 @@ module Http { xhr.addEventListener('load', async (event) => { delete this._requests[#{uid}] - let header = xhr.getResponseHeader("content-type"); + let contentType = xhr.getResponseHeader("content-type"); let responseText = await xhr.response.text(); let body; - if (header.startsWith("text/html")) { - body = #{Http.ResponseBody::Html(`responseText`)}; - } else if (header.startsWith("application/json")) { + if (contentType.startsWith("text/html")) { + const object = + (new DOMParser()).parseFromString(responseText, "text/html"); + + const errorNode = + doc.querySelector("parsererror"); + + if (errorNode) { + body = #{Http.ResponseBody::Text(`responseText`)}; + } else { + body = #{Http.ResponseBody::HTML(`object`)}; + } + } else if (contentType.startsWith("application/xml")) { + const object = + (new DOMParser()).parseFromString(responseText, "application/xml"); + + const errorNode = + doc.querySelector("parsererror"); + + if (errorNode) { + body = #{Http.ResponseBody::Text(`responseText`)}; + } else { + body = #{Http.ResponseBody::XML(`object`)}; + } + } else if (contentType.startsWith("application/json")) { try { - body = #{Http.ResponseBody::Json(`JSON.parse(responseText)`)}; + body = #{Http.ResponseBody::JSON(`JSON.parse(responseText)`)}; } catch (e) { body = #{Http.ResponseBody::Text(`responseText`)}; } - } else if (header.startsWith("text/")) { + } else if (contentType.startsWith("text/")) { body = #{Http.ResponseBody::Text(`responseText`)}; } if (!body) { const parts = #{Url.parse(request.url).path}.split('/'); - body = #{Http.ResponseBody::File(`new File([xhr.response], parts[parts.length - 1], { type: header })`)}; + body = #{Http.ResponseBody::File(`new File([xhr.response], parts[parts.length - 1], { type: contentType })`)}; } resolve(#{Result::Ok({ From 9138b3d682ec1e763e9c3c22960d84dee6420cc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szikszai=20Guszt=C3=A1v?= Date: Thu, 27 Apr 2023 15:39:44 +0100 Subject: [PATCH 3/3] Update core/source/Http.mint Co-authored-by: Sijawusz Pur Rahnama --- core/source/Http.mint | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/source/Http.mint b/core/source/Http.mint index 3931abb84..a3ab0f38e 100644 --- a/core/source/Http.mint +++ b/core/source/Http.mint @@ -325,7 +325,7 @@ module Http { xhr.addEventListener('load', async (event) => { delete this._requests[#{uid}] - let contentType = xhr.getResponseHeader("content-type"); + let contentType = xhr.getResponseHeader("Content-Type"); let responseText = await xhr.response.text(); let body;