A comprehensive guide to making HTTP requests in Deno

Mayank C
Tech Tonic

--

Introduction

Making HTTP requests is a basic work of web apps, especially in microservices architecture, where a front facing service depends on a number of other backend services to get the job done. A single request to a front facing service could result in numerous internal HTTP requests.

As Deno follows web standard wherever possible, the core runtime offers HTTP client through the standard browser fetch API. The web standard fetch API is a well known API that’s supported by all the modern browsers. Deno offers the exact same API, including the exact same interfaces.

There is a detailed documentation of the fetch API here. This article focuses more on use-cases/examples. In this article, we’ll learn about:

  • Making basic requests with fetch
  • Advanced options like timeout, proxying, etc.
  • Preparing query params & request headers
  • Preparing request body encoded with text, URL encoded, JSON, multipart/form-data, and binary data
  • File upload
  • Processing basic response status code & headers
  • Processing response body encoded with text, URL encoded, JSON, multipart/form-data, and binary data
  • File download
  • HTTPS

Basics

The web standard fetch API supports making all kind of HTTP requests. For simple GET requests, the fetch API can be used just with a URL. Let’s have a look at the input & output:

The inputs can be provided in three ways:

  • URL: For simple GET requests, a simple URL can be provided
  • Request: For more complex cases like headers, body, etc., a web standard Request object can be provided
  • URL+options: For medium cases like making simple POST requests with or without body (this is almost like passing Request object)

The output is always presented as:

  • Response: The web standard Response object that contains everything like status code, headers, and optional body.

HTTP methods

The following is the complete list of allowed HTTP methods:

  • DELETE
  • HEAD
  • OPTIONS
  • GET
  • POST
  • PUT
  • PATCH

The following methods are valid HTTP methods, but aren’t allowed to be called from user (because they’re used internally):

  • CONNECT
  • TRACE
  • TRACK

An exception is raised if internal methods are used:

TypeError: Method is forbidden.

GET

A URL can be passed to make simple GET requests. The URL should be in full format like:

http://localhost/a/b/c
http://localhost:8080/a/b/c
https://localhost/a/bc
https://deno.land

Here is an example of making a simple GET request through fetch:

Code =>const resp=await fetch("http://localhost:8080");
const respData = await resp.text();
Decoded response body (respData) =>Hello

We’ll go over response handling a bit later.

GET/HEAD requests can’t contain body

POST

A Request object or options needs to be provided to make a POST request. At a minimum, the method attribute must be set. For POST requests, the attribute method needs to be set to POST.

Code =>const resp = await fetch("http://localhost:8080", {
method: "POST",
});
const respData = await resp.text();
Decoded response body (respData) =>Hello

The same can be extended to other methods like DELETE, PATCH, etc.

const resp = await fetch("http://localhost:8080", {
method: "DELETE",
});

While passing an options object works for most of the simple use cases, the other way is to build a Request object and then pass it to the fetch API. The latter way is useful if the request building happens in a number of steps involving query params, headers, body, etc. Or, the latter is also useful if request is built in a different place and fetch is called in a different place.

Code =>Code =>const req = new Request("http://localhost:8080", {
method: "POST",
});
const resp = await fetch(req);
const respData = await resp.text();
Decoded response body (respData) =>Hello

As mentioned earlier, except GET & HEAD, all other methods can contain a body. We’ll see request body building shortly.

Fetching local files

The same fetch API can also be used to fetch local files. This way, the code is the same whether the fetch is happening from another service or local disk. For local fetches, the URL must be in file:// format so that Deno can get it from the disk.

Fetching a local file is equivalent to reading a local file

Code =>const resp = await fetch("file:///var/tmp/testdata/sample.txt");
const respData = await resp.text();
Decoded response body (respData) =>Learning Deno Is Fun!

Only GET is supported for fetching local files. Any other method results in error:

Code =>const resp = await fetch("file:///var/tmp/testdata/sample.txt", {
method: "POST",
});
Error =>//TypeError: Fetching files only supports the GET method. Received POST.

Advanced options

The fetch API can be extended by providing additional options for three use-cases:

  • Timeouts
  • Proxying
  • Custom certificates

We’ll have a look at timeouts & proxying in this section. We’ll look at custom certificates in the last section.

Timeouts

By default, the fetch request never times out. This is not acceptable for production systems, as the caller would keep waiting till a gateway timeout happens somewhere between caller and callee.

A pending fetch can be cancelled by using web standard AbortController. Unfortunately, enabling timeouts isn’t as simple as passing a timeout value to the fetch API. There are a number of steps in getting the job done. Let’s have a look at them:

  • Create an AbortController
  • Get the signal handler of the AbortController
  • Pass the signal handler to the fetch API in the request object or options object
  • When required, abort the AbortController (usually through setTimeout or delayed promise)
  • This would send an abort signal to the fetch API
  • The pending fetch API would abort and an AbortError would be raised

It looks like a lot of work, but in terms of code, the work isn’t that much. The following is an example of cancelling fetch after 5 seconds:

const ac = new AbortController();
const id = setTimeout(() => ac.abort(), 1000);
const resp = await fetch("http://localhost:8080", { signal: ac.signal });
clearTimeout(id);

Here is an example of the error when the fetch gets cancelled:

AbortError: Ongoing fetch was aborted.

Usually the fetch call will be inside a single function, therefore that function (like doFetch below) can take timeout as input and return an appropriate response object. If unspecified, the default timeout is 10 seconds.

const resp = await doFetch("http://localhost:8080", 5000);
console.log(resp.status);
export async function doFetch(
url: string,
timeout: number = 10000,
): Promise<Response> {
let res: Response;
try {
const c = new AbortController();
const id = setTimeout(() => c.abort(), timeout);
res = await fetch(url, { signal: c.signal });
clearTimeout(id);
return res;
} catch (err) {
if (err instanceof DOMException && err.name === "AbortError") {
res = new Response(null, { status: 408 });
} else {
res = new Response(null, { status: 503 });
}
}
return res;
}

Here is the output for a successful case and timeout:

//successful case
resp.status; //200
//timeout case
resp.status; //408

Proxying

In large scale distributed systems, the external services (the ones that goes out of the internal network) might be behind secure proxies. The fetch API provides an option to make HTTP request via proxy with optional authentication. There are two steps involved in working with proxies:

  • Create a custom HTTP client with proxy URL & optional credentials
  • Call the fetch API and pass the custom HTTP client

When using proxy, Deno will make a request to proxy instead and passes target host alongwith optional credentials to authorize with proxy.

Code =>const client = Deno.createHttpClient({
proxy: {
url: "http://localhost:8080",
basicAuth: {
username: "proxyUser1",
password: "proxyUser1Password"
},
},
});
const res = await fetch("http://deno.land/std/", {
client,
});
Status code (res.status) =>200

At the proxy, the request would contain the following relevant data:

URL => GET http://deno.land/std/Relevant request headers =>host: deno.land,
proxy-authorization: Basic cHJveHlVc2VyMTpwcm94eVVzZXIxUGFzc3dvcmQ=

The proxy-authorization header contains username/password in base64 format.

Query params & request headers

In this sub-section, we’ll learn how to prepare query params & request headers.

Query params

Query parameters are a set of parameters attached to the end of an URL. They are extensions of the URL that are used to help define specific content or actions based on the data being passed. To append query params to the end of a URL, a ‘?’ is added followed immediately by one or more query parameters delimited by &. The query params are URL encoded.

http://localhost:5000/?a=b&c=1&d=e

Deno supports web standard URLSearchParams interface that can be used to build query params. An object of type URLSearchParams can be appended directly to the URL given to fetch. Deno takes care of serialization.

To set query params:

  • Prepare a URLSearchParams object
  • Append it directly to the URL string with a trailing ‘?’

The following code shows how to build query params & then pass them to the fetch API:

Code =>const q = new URLSearchParams({
someText: "some param with spaces",
someNumber: "100",
});
q.set("someCurrency", "$1000");
const res = await fetch("http://localhost:8080/?" + q);

For correct encoding, ? must be present at the end of URL

For the above example, the following URL is received by the server:

http://localhost:8080/?someText=some+param+with+spaces&someNumber=100&someCurrency=%241000

As we can see, query params are URL encoded.

Request headers

The HTTP headers are used to pass additional information between the clients and the server through the request and response headers. All the headers are case-insensitive, headers fields are separated by colon, key-value pairs in clear-text string format. By default, Deno sends the following request headers:

Code =>const res = await fetch("http://localhost:8080/"); //GETDefault request headers =>accept: */*
accept-encoding: gzip, br
host: localhost:8080
user-agent: Deno/1.17.0
const res = await fetch("http://localhost:8080/", {
method: "POST",
});
Default request headers =>accept: */*,
accept-encoding: gzip, br
host: localhost:8080
user-agent: Deno/1.17.0
content-length: 0

To set additional/custom request headers, the web standard Headers interface can be used. There are two ways to set request headers:

  • Set via options object
Code =>const res = await fetch("http://localhost:8080/", {
headers: {
someHeader: "someValue1",
"some-custom-header": "some-custom-value",
},
});
Request headers =>accept: */*,
accept-encoding: gzip, br
host: localhost:8080
user-agent: Deno/1.17.0
some-custom-header: some-custom-value
someheader: someValue1
  • Set in a separate header object & then give it to the fetch
Code =>const headers = new Headers({
someHeader: "someValue1",
});
headers.set("some-custom-header", "some-custom-value");
const res = await fetch("http://localhost:8080/", {
headers,
});
Request headers =>accept: */*,
accept-encoding: gzip, br
host: localhost:8080
user-agent: Deno/1.17.0
some-custom-header: some-custom-value
someheader: someValue1

Request body

Some of the HTTP requests like POST, PUT, PATCH, DELETE could contain a request body. A request body contains data associated with the request like, content of an HTML form, files, etc. The presence of the body and its size is specified by the start-line and HTTP headers.

The acceptable request body formats are generally published in server’s API documentation. The body format could be anything as long as the format type is indicated by the content-type header.

The commonly used formats are:

  • Text
  • JSON
  • URL encoded
  • Multipart form data
  • Binary

Deno folllows the web standard Body interface. The request body can be specified in the fetch options via body attribute:

const res=await fetch('http://localhost:8000', {
body: <body to be sent>
});

Let’s understand how to build a body with the commonly used formats.

Request body can’t be passed with GET/HEAD methods

Text

A textual body (aka string) can given directly to the fetch API as the body attribute. Deno’ll set the content-type header to text/plain and content-length header to the length of the passed string.

Code =>const res = await fetch("http://localhost:8080/", {
method: "POST",
body: "Hello from me!",
});
Relevant request headers =>content-length: 14
content-type: text/plain;charset=UTF-8
Request body =>Hello from me!

If the type is text, but the file format is different like HTML, then the content-type needs to be set by the caller.

Code =>const res = await fetch("http://localhost:8080/", {
method: "POST",
headers: {
"content-type": "text/html",
},
body: "<HTML>BODY><H1>Hello</H1></BODY></HTML>",
});
Relevant request headers =>content-length: 39
content-type: text/html
Request body =><HTML>BODY><H1>Hello</H1></BODY></HTML>

JSON

Though being a very popular format, JSON format isn’t supported by the web standard body interface. The JSON object needs to be serialized into string before using it as body. The content-type header needs to be set by the user. If content-type isn’t set explicitly, it’ll default to text/plain.

Code =>const res = await fetch("http://localhost:8080/", {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({ a: 1, b: [1, 2], c: { d: "e" } }),
});
Relevant request headers =>content-length: 31,
content-type: application/json
Request body =>{"a":1,"b":[1,2],"c":{"d":"e"}}

URL encoded

A URL encoded body is commonly used for simple form submissions. This is an old, but still a popular format. The web standard URLSearchParams interface can again be used to prepare a URL encoded body. An object of type URLSearchParams can be given directly as the body attribute. The content-type and content-length would be set by Deno.

Code => const ud = new URLSearchParams({
a: "b",
});
ud.set("c", "2");
const res = await fetch("http://localhost:8080/", {
method: "POST",
body: ud,
});
Relevant request headers =>content-length: 7
content-type: application/x-www-form-urlencoded;charset=UTF-8
Request body =>a=b&c=2

Multipart form data

A multipart form data body is used to send a mixed type of body that could contain simple KVs and/or files. A multipart/form-data contains a series of parts. Each part is expected to contain a content-disposition header where the
disposition type is “form-data”, and where the disposition contains
an (additional) parameter of “name”, where the value of that
parameter is the original field name in the form. In other words, each value is sent as a block of data (“body part”), with a user agent-defined delimiter (“boundary”) separating each part. The keys are given in the Content-Disposition header of each part.

The web standard FormData interface can be used to prepare a multipart form data. An object of type FormData can be given directly as the body attribute. The content-length & content-type headers would be set by Deno.

Let’s look at two types:

  • Simple KVs: To set simple KVs, only FormData’s set API is enough
Code =>const fd = new FormData();
fd.set("a", "b");
fd.set("c", "2");
const res = await fetch("http://localhost:8080/", {
method: "POST",
body: fd,
});
Relevant request headers =>content-length: 202,
content-type: multipart/form-data; boundary=----1270478600501399366852311683
Request body =>------1270478600501399366852311683
Content-Disposition: form-data; name="a"
b
------1270478600501399366852311683
Content-Disposition: form-data; name="c"
2
------1270478600501399366852311683--
  • KVs and files: To use files, the web standard File interface need to be used as the value for FormData’s set API
Code =>const fileData = await Deno.readFile("/var/tmp/testdata/sample.txt");
const fd = new FormData();
fd.set("someKey", "someValue");
fd.set(
"file1",
new File([fileData], "someTextFile.txt", { type: "text/plain" }),
);
const res = await fetch("http://localhost:8080", {
method: "POST",
body: fd,
});
Relevant request headers =>content-length: 296,
content-type: multipart/form-data; boundary=----6076278287040079490387636163
Request body =>------6076278287040079490387636163
Content-Disposition: form-data; name="someKey"
someValue
------6076278287040079490387636163
Content-Disposition: form-data; name="file1"; filename="someTextFile.txt"
Content-Type: text/plain
Learning Deno Is Fun!------6076278287040079490387636163--

Binary data

An Uint8Array containing some arbitrary binary data can be used directly as the body attribute. Deno’ll set the content-length to the length of the binary data. The content-type needs to be set by the user.

Code =>//Fill ascii character A (65) in a byte array
const bd = new Uint8Array(10).fill(65);
const res = await fetch("http://localhost:8080", {
method: "POST",
headers: {
"content-type": "application/octet-stream",
},
body: bd,
});
Relevant request headers =>content-length: 10
content-type: application/octet-stream
Request body =>AAAAAAAAAA

File upload

For this article, file upload is defined as sending a local file through HTTP request. In addition to the types mentioned in previous section, the body part of the Request object can also take a stream (ReadableStream) as input. The source of stream could be a file, thereby achieveing the goal of uploading file.

The Request object supports file upload via Blob or web’s ReadableStream. Deno supports ReadableStream, but this isn’t commonly used in Deno’s world. Deno code commonly uses properietary Deno.Reader interface instead of web standard ReadableStream. Fortunatley, Deno’s standard library’s streams module provides an easy way to convert Deno.Reader into a ReadableStream that can be given to the Request object of the fetch API.

The following steps are involved in uploading files:

  • Create a Deno.Reader by opening a file using Deno.open
  • Convert it to ReadableStream using readableStreamFromReader utility
  • Use the ReadableStream as the body attribute of the fetch API call

An important point to note: As we’re dealing with streams, Deno will not be able to set the content-length and content-type headers. Deno.stat API can be used to get the length of the file.

Code =>import { readableStreamFromReader as toStream } from "https://deno.land/std/io/mod.ts";const filePath = "/var/tmp/testdata/sample.pdf";
const size = (await Deno.stat(filePath)).size;
const f = await Deno.open(filePath);
const req = new Request("http://localhost:8080", {
method: "POST",
headers: {
"content-type": "application/pdf",
"content-length": `${size}`,
},
body: toStream(f),
});
const resp = await fetch(req);
Relevant request headers =>content-length: 69273
content-type: application/pdf
Request body =><< binary data suppressed >>

That’s all about preparing & sending request through fetch. Let’s move to response handling.

Response status & headers

Unless an error is encountered, the fetch API always provides a web standard Response object. The response object contains everything that has been received from the server. This includes:

  • Response status code
  • Response status text
  • Response headers
  • Response body

In this section, we’ll understand how to get the response status code and headers. In the following section, we’ll understand how to get response body in a variety of formats like text, JSON, URL encoded, multipart form data, and binary data.

Response status

From Wiki, HTTP status codes are issued by a server in response to a client’s request made to the server. The first digit of the status code specifies one of five standard classes of responses: 1xx informational, 2xx success, 3xx redirection, 4xx client errors, & 5xx server errors.

The web standard Response object contains two pieces of information related to status codes:

  • status: The integer status code (this is always present)
  • statusText: The human readable equivalent of the integer status code (this is optional)
const resp = await fetch("http://localhost:8080");
resp.status; //200
resp.statusText; //OK

Here is an example of a request missing some parameters:

const resp = await fetch("http://localhost:8080/somepath/xyz");
resp.status; //400
resp.statusText; //Bad Request

Response headers

From Wiki, HTTP header fields are a list of strings sent and received by both the client program and server on every HTTP request and response. These headers are usually invisible to the end-user and are only processed or logged by the server and client applications.

The Response object offers all the headers via the web standard Headers interface.

There are three APIs to work with headers:

  • has: Returns true if a header exists, false otherwise
const resp = await fetch("http://localhost:8080");
resp.headers.has("content-type"); //true
resp.headers.has("content-length"); //true
resp.headers.has("x-request-id"); //false
  • get: The get function can be used to get the value for a given header name (CSV is returned only if there are multiple headers with the same name).
const resp = await fetch("http://localhost:8080");
resp.headers.get("content-type"); //text/plain;charset=UTF-8
resp.headers.get("content-length"); //5
resp.headers.get("x-request-id"); //null
  • entries: The entries function can be used to iterate over all the headers
const resp = await fetch("http://localhost:8080");
console.log(resp.status, resp.statusText);
for (const h of resp.headers.entries()) {
console.log(h[0], h[1]);
}
//content-length 5
//content-type text/plain;charset=UTF-8
//date Sat, 18 Dec 2021 19:29:41 GMT

Response body

Most of the successful HTTP responses contains a body, except for status code 204 that indicates a successful execution without anything to return. In some cases, even error responses contains a body with additional information about the error. For example — an HTTP status 400 Bad Request could contain a body indicating what exactly was wrong with the request.

Just like the request body, the response body could be formatted in a variety of formats like:

  • Text
  • JSON
  • URL encoded
  • Multipart form data
  • Binary

The formatting of the response body is indicated by the content-type header. The response object comes with APIs to decode the request body from a particular format.

It is caller’s responsibility to use the right decoding API

Let’s have a look at response body decoding with commonly used formats.

All the decoding APIs are async

Text

A textual response body can be decoded by using the text API. The text API returns a string.

Raw response body =>HelloCode => const resp = await fetch("http://localhost:8080");
const respData = await resp.text();
Decoded response body (respData) =>Hello

The text API can be used to read any type of body that is suitable to fit into string. This API can be useful for logging.

JSON

Unlike request body prepartion, where JSON needed to be serialized, a JSON encoded response body can be decoded directly using json API. The JSON API produces a regular JavaScript object.

Raw response body =>{"a":1,"b":{"c":"d"}}Code =>const resp = await fetch("http://localhost:8080");
const respData = await resp.json();
JSON response body (respData) =>{ a: 1, b: { c: "d" } }

URL encoded

A URL encoded body can be decoded using formData API. The API returns a web standard FormData object that offers APIs like has, get, and entries (similar to the headers interface).

Raw response body =>a=b&c=hello+worldCode =>const resp = await fetch("http://localhost:8080");
const respData = await resp.formData();
respData.has('a'); //true
respData.has('c')); //true
respData.has('d')); //false
respData.get('a'); //b
respData.get('c')); //hello world
respData.get('d')); //null
for (const d of respData.entries()) {
console.log(d[0], d[1]);
}
//a b
//c hello world

Multipart form data

A multipart/form-data encoded repsonse body can also be decoded using the same API as URL encoded data: formData. In both cases, a FormData object is returned that offers APIs like has, get, and entries to go through all the parts of the response body.

Raw response body =>------5716218068309384961320469072
Content-Disposition: form-data; name="a"
b
------5716218068309384961320469072
Content-Disposition: form-data; name="file1"; filename="someTextFile.txt"
Content-Type: text/plain
Learning Deno Is Fun!------5716218068309384961320469072--Code =>const resp = await fetch("http://localhost:8080");const respData2 = await resp.clone();
console.log(await respData2.text());
const respData = await resp.formData();
respData.get('a'); //b
respData.get('file1'); //File { size: 22, type: "text/plain" }
for (const d of respData.entries()) {
console.log(d[0], d[1]);
if(d[1] instanceof File)
console.log(d[1].name, 'content:', await d[1].text());
}
//a b
//file1 File { size: 22, type: "text/plain" }
//someTextFile.txt content: Learning Deno Is Fun!

Binary data

Any arbitrary binary data can be read into ArrayBuffer using arrayBuffer API, that can be viewed using a typed array.

Raw response body (shown as hex string) =>4142434445464748494aCode =>const resp = await fetch("http://localhost:8080");
const respData = new Uint8Array(await resp.arrayBuffer());
const strData = new TextDecoder().decode(respData);
Decoded response body (respData) =>Uint8Array(10) [65, 66, 67, 68, 69, 70, 71, 72, 73, 74]Decoded repsonse body (strData) =>ABCDEFGHIJ

File download

In addition to file upload, one of the other common use case of the fetch API is to download files. The Response object contains the received file via Blob or web’s ReadableStream. Deno supports ReadableStream, but this isn’t commonly used in Deno’s world. Deno code commonly uses properitary Deno.Reader instead of the standard ReadableStream. Fortunatley, Deno’s standard library’s streams module provides an easy way to convert web’s ReadableStream into Deno.Reader that can be given to used with the standard library’s copy API to write into a file.

The following steps are involved in downloading files:

  • Get body from the Response object (resp.body)
  • Get the default reader from the body (body.getReader)
  • Convert default reader into Deno.Reader (readerFromStreamReader)
  • Use Deno.Reader whereever the Reader interface supported (copy)
Raw response body =><HTML><BODY><H1>HELLO!</H1></BODY></HTML>Code =>import { readerFromStreamReader as toRdr} from "https://deno.land/std/streams/mod.ts";
import { copy } from "http://deno.land/std/io/mod.ts";
const resp = await fetch("http://localhost:8080/sample.html");
if (!resp.body) {
console.error("No response body found");
} else {
const src = toRdr(resp.body.getReader());
const dest = await Deno.open("./downloadedFile.html", {
create: true,
write: true,
});
await copy(src, dest);
dest.close();
}
Processed response body (dest) =>$ cat ./downloadedFile.html
<HTML><BODY><H1>HELLO!</H1></BODY></HTML>

HTTPS

This is the last section of this article. In this section, we’ll learn how to use fetch to make HTTPS requests. The only usage level difference between HTTP & HTTPS is an optional addition of certificates. Once a secure connection is established with the server, the request preparation and response handling is the same. For the user, there is very little additional work.

Basic requests

The exact same fetch API can be used as is to make HTTPS requests. The only thing new is that the URL starts with https:// instead of http://.

Code =>const resp = await fetch("https://deno.land");
console.log(
"Status:",
resp.status,
"Received size:",
(await resp.text()).length,
);
Received response =>Status: 200 Received size: 24452

As we can see, there is nothing new for the user except that the URL starts with https://. Let’s see another example:

Code =>const resp = await fetch("https://google.com");Received response =>Status: 200 Received size: 15081

The basic fetch case works easily because the server’s certificates are signed by a well known certificate authority. A certificate authority is an entity that issues digital certificates that certifies the ownership of a public key by the named subject of the certificate. The well known certificates are trusted.

The fetch usage differs when there are self-signed certificates. Let’s see how to work with them.

Self-signed certificates

Sometimes servers have self-signed certificates. A self-signed certificate is a security certificate that is not signed by a known certificate authority (CA). These certificates are generated by the organization owning the server and do not cost money. However, they do not provide all of the security properties that certificates signed by a CA aim to provide.

Self-signed certificates are considered as a security loophole

However, in some cases, it might be okay to connect to trusted servers that have self-signed certificates. In such cases, there are two options:

  • Using server provided CA certificate to verify
  • Accepting the self-signed certificate only

Some servers may have a certificate signed by some unknown CA. In such cases, server may give their CA’s certificate that can be used to verify the server certificate. In the following example, we’ve used s.pem file that contains the root certificate, the highest certificate on the chain, which is self-signed by the primary CA.

Code =>const caCert = await Deno.readTextFile("./s.pem");
const client = Deno.createHttpClient({
caCerts: [caCert],
});
const resp = await fetch("https://localhost:8080", {client});
const respData = await resp.text();
Decoded response body (respData) =>Secure hello

In some cases, there is just a self-signed certificate with no CA. By default, the fetch API raises error whenever it encounters a self-signed certificate.

Code =>const resp = await fetch("https://localhost:8080");
const respData = await resp.text();
console.log('Response body:', respData);
Runtime error =>$ deno run --allow-net=localhost --unstable app.tsTypeError: error sending request for url (https://localhost:8080/): error trying to connect: invalid peer certificate contents: invalid peer certificate: UnknownIssuer

To enable acceptance of self-signed certificate, a command line option — unsafely-ignore-certificate-errors needs to be provided. An error line is printed whenever certificate validation is intentionally disabled.

$ deno run --allow-net=localhost --unstable --unsafely-ignore-certificate-errors app.ts 
DANGER: TLS certificate validation is disabled for all hostnames
Response body: Secure hello

For better security, it is possible to disable certificate validation for certain hosts only:

$ deno run --allow-net=localhost --unstable --unsafely-ignore-certificate-errors=localhost app.ts 
DANGER: TLS certificate validation is disabled for all hostnames
Response body: Secure hello

That’s all about making HTTP requests in Deno. A similar article here covers writing HTTP servers in detail.

--

--