├── .gitignore
├── test
├── polyfills.js
└── gleam_fetch_test.gleam
├── .github
└── workflows
│ └── test.yml
├── gleam.toml
├── CHANGELOG.md
├── manifest.toml
├── README.md
├── src
├── gleam_fetch_ffi.mjs
└── gleam
│ ├── fetch
│ └── form_data.gleam
│ └── fetch.gleam
└── LICENCE
/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 | node_modules
3 |
--------------------------------------------------------------------------------
/test/polyfills.js:
--------------------------------------------------------------------------------
1 | import { default as fetch, Headers, Request, Response } from "node-fetch";
2 |
3 | if (!globalThis.fetch) {
4 | globalThis.fetch = fetch;
5 | globalThis.Headers = Headers;
6 | globalThis.Request = Request;
7 | globalThis.Response = Response;
8 | }
9 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: test
2 |
3 | on:
4 | push:
5 | branches:
6 | - "main"
7 | - "v*.*.*"
8 | pull_request:
9 |
10 | jobs:
11 | test:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v3
15 | - uses: actions/setup-node@v2
16 | with:
17 | node-version: "19.3.0"
18 | - uses: erlef/setup-beam@v1
19 | with:
20 | otp-version: false
21 | gleam-version: "1.9.1"
22 | - run: gleam test
23 | - run: gleam format --check src test
24 |
--------------------------------------------------------------------------------
/gleam.toml:
--------------------------------------------------------------------------------
1 | name = "gleam_fetch"
2 | version = "1.3.0"
3 | licences = ["Apache-2.0"]
4 | description = "Make HTTP requests in Gleam JavaScript with Fetch"
5 | target = "javascript"
6 | gleam = ">= 1.9.0"
7 |
8 | repository = { type = "github", user = "gleam-lang", repo = "fetch" }
9 | links = [
10 | { title = "Website", href = "https://gleam.run" },
11 | { title = "Sponsor", href = "https://github.com/sponsors/lpil" },
12 | ]
13 |
14 | [dependencies]
15 | gleam_http = ">= 3.1.0 and < 5.0.0"
16 | gleam_javascript = ">= 0.3.0 and < 2.0.0"
17 | gleam_stdlib = ">= 0.32.0 and < 2.0.0"
18 |
19 | [dev-dependencies]
20 | gleeunit = ">= 1.0.0 and < 2.0.0"
21 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## v1.3.0 - 2025-03-29
4 |
5 | - Updated for Gleam v1.9.0.
6 |
7 | ## v1.2.0 - 2025-03-20
8 |
9 | - Add support for formdata reading and editing.
10 |
11 | ## v1.1.1 - 2025-02-06
12 |
13 | - Relaxed the `gleam_http` requirement to permit v4.
14 |
15 | ## v1.1.0 - 2024-11-19
16 |
17 | - Support for formdata bodies added.
18 |
19 | ## v1.0.1 - 2024-05-12
20 |
21 | - Internal structural changes. No user facing changes.
22 |
23 | ## v1.0.0 - 2024-04-22
24 |
25 | - Added the `send_bits` function.
26 |
27 | ## v0.4.0 - 2024-03-07
28 |
29 | - Added the `read_bytes_body` function.
30 |
31 | ## v0.3.1 - 2024-01-16
32 |
33 | - Relaxed version constraint on `gleam_stdlib` to permit 0.x or 1.x.
34 |
35 | ## v0.3.0 - 2023-11-06
36 |
37 | - Updated for Gleam v0.32.0.
38 |
39 | ## v0.2.1 - 2023-09-30
40 |
41 | - `gleam_stdlib` is now listed as a direct dependency.
42 |
43 | ## v0.2.0 - 2023-08-03
44 |
45 | - Updated for Gleam v0.30.0.
46 |
47 | ## v0.1.0 - 2022-12-29
48 |
49 | - Initial release
50 |
--------------------------------------------------------------------------------
/manifest.toml:
--------------------------------------------------------------------------------
1 | # This file was generated by Gleam
2 | # You typically do not need to edit this file
3 |
4 | packages = [
5 | { name = "gleam_http", version = "4.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "0A62451FC85B98062E0907659D92E6A89F5F3C0FBE4AB8046C99936BF6F91DBC" },
6 | { name = "gleam_javascript", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "EF6C77A506F026C6FB37941889477CD5E4234FCD4337FF0E9384E297CB8F97EB" },
7 | { name = "gleam_stdlib", version = "0.54.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "723BA61A2BAE8D67406E59DD88CEA1B3C3F266FC8D70F64BE9FEC81B4505B927" },
8 | { name = "gleeunit", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "0E6C83834BA65EDCAAF4FE4FB94AC697D9262D83E6F58A750D63C9F6C8A9D9FF" },
9 | ]
10 |
11 | [requirements]
12 | gleam_http = { version = ">= 3.1.0 and < 5.0.0" }
13 | gleam_javascript = { version = ">= 0.3.0 and < 2.0.0" }
14 | gleam_stdlib = { version = ">= 0.32.0 and < 2.0.0" }
15 | gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Gleam Fetch
2 |
3 |
4 |
5 |
6 | Make HTTP requests in Gleam JavaScript with Fetch.
7 |
8 | ```sh
9 | gleam add gleam_fetch@1 gleam_http
10 | ```
11 |
12 | ```gleam
13 | import gleam/fetch
14 | import gleam/http/request
15 | import gleam/http/response
16 | import gleam/javascript/promise
17 |
18 | pub fn main() {
19 | let assert Ok(req) = request.to("https://example.com")
20 |
21 | // Send the HTTP request to the server
22 | use resp <- promise.try_await(fetch.send(req))
23 | use resp <- promise.try_await(fetch.read_text_body(resp))
24 |
25 | // We get a response record back
26 | resp.status
27 | // -> 200
28 |
29 | response.get_header(resp, "content-type")
30 | // -> Ok("text/html; charset=UTF-8")
31 |
32 | promise.resolve(Ok(Nil))
33 | }
34 | ```
35 |
36 | Documentation can be found at [https://hexdocs.pm/gleam_fetch](https://hexdocs.pm/gleam_fetch).
37 |
38 | > [!WARNING]
39 | > If you are running your Gleam project on the Erlang target (the default for
40 | > new Gleam projects) then you will want to use a different library which can
41 | > run on Erlang, such as [`gleam_httpc`](https://github.com/gleam-lang/httpc).
42 |
--------------------------------------------------------------------------------
/src/gleam_fetch_ffi.mjs:
--------------------------------------------------------------------------------
1 | import { Ok, Error, List, toBitArray, toList } from "./gleam.mjs";
2 | import { to_string as uri_to_string } from "../gleam_stdlib/gleam/uri.mjs";
3 | import { method_to_string } from "../gleam_http/gleam/http.mjs";
4 | import { to_uri } from "../gleam_http/gleam/http/request.mjs";
5 | import { Response } from "../gleam_http/gleam/http/response.mjs";
6 | import {
7 | NetworkError,
8 | InvalidJsonBody,
9 | UnableToReadBody,
10 | } from "../gleam_fetch/gleam/fetch.mjs";
11 |
12 | export async function raw_send(request) {
13 | try {
14 | return new Ok(await fetch(request));
15 | } catch (error) {
16 | return new Error(new NetworkError(error.toString()));
17 | }
18 | }
19 |
20 | export function from_fetch_response(response) {
21 | return new Response(
22 | response.status,
23 | List.fromArray([...response.headers]),
24 | response
25 | );
26 | }
27 |
28 | function request_common(request) {
29 | let url = uri_to_string(to_uri(request));
30 | let method = method_to_string(request.method).toUpperCase();
31 | let options = {
32 | headers: make_headers(request.headers),
33 | method,
34 | };
35 | return [url, options]
36 | }
37 |
38 | export function to_fetch_request(request) {
39 | let [url, options] = request_common(request)
40 | if (options.method !== "GET" && options.method !== "HEAD") options.body = request.body;
41 | return new globalThis.Request(url, options);
42 | }
43 |
44 | export function form_data_to_fetch_request(request) {
45 | let [url, options] = request_common(request)
46 | if (options.method !== "GET" && options.method !== "HEAD") options.body = request.body;
47 | // Remove `content-type`, because the browser will add the correct header by itself.
48 | delete options.headers['content-type']
49 | return new globalThis.Request(url, options);
50 | }
51 |
52 | export function bitarray_request_to_fetch_request(request) {
53 | let [url, options] = request_common(request)
54 | if (options.method !== "GET" && options.method !== "HEAD") options.body = request.body.rawBuffer;
55 | return new globalThis.Request(url, options);
56 | }
57 |
58 | function make_headers(headersList) {
59 | let headers = new globalThis.Headers();
60 | for (let [k, v] of headersList) headers.append(k.toLowerCase(), v);
61 | return headers;
62 | }
63 |
64 | export async function read_bytes_body(response) {
65 | let body;
66 | try {
67 | body = await response.body.arrayBuffer()
68 | } catch (error) {
69 | return new Error(new UnableToReadBody());
70 | }
71 | return new Ok(response.withFields({ body: toBitArray(new Uint8Array(body)) }));
72 | }
73 |
74 | export async function read_text_body(response) {
75 | let body;
76 | try {
77 | body = await response.body.text();
78 | } catch (error) {
79 | return new Error(new UnableToReadBody());
80 | }
81 | return new Ok(response.withFields({ body }));
82 | }
83 |
84 | export async function read_json_body(response) {
85 | try {
86 | let body = await response.body.json();
87 | return new Ok(response.withFields({ body }));
88 | } catch (error) {
89 | return new Error(new InvalidJsonBody());
90 | }
91 | }
92 |
93 | // FormData functions.
94 |
95 | export function newFormData() {
96 | return new FormData()
97 | }
98 |
99 | function cloneFormData(formData) {
100 | const f = new FormData()
101 | for (const [key, value] of formData.entries()) f.append(key, value)
102 | return f
103 | }
104 |
105 | export function appendFormData(formData, key, value) {
106 | const f = cloneFormData(formData)
107 | f.append(key, value)
108 | return f
109 | }
110 |
111 | export function setFormData(formData, key, value) {
112 | const f = cloneFormData(formData)
113 | f.set(key, value)
114 | return f
115 | }
116 |
117 | export function appendBitsFormData(formData, key, value) {
118 | const f = cloneFormData(formData)
119 | f.append(key, new Blob([value.rawBuffer]))
120 | return f
121 | }
122 |
123 | export function setBitsFormData(formData, key, value) {
124 | const f = cloneFormData(formData)
125 | f.set(key, new Blob([value.rawBuffer]))
126 | return f
127 | }
128 |
129 | export function deleteFormData(formData, key) {
130 | const f = cloneFormData(formData)
131 | f.delete(key)
132 | return f
133 | }
134 |
135 | export function getFormData(formData, key) {
136 | const data = [...formData.getAll(key)]
137 | return toList(data.filter(value => typeof value === 'string'))
138 | }
139 |
140 | export async function getBitsFormData(formData, key) {
141 | const data = [...formData.getAll(key)]
142 | const encode = new TextEncoder()
143 | const blobs = data.map(async (value) => {
144 | if (typeof value === 'string') {
145 | const encoded = encode.encode(value)
146 | return toBitArray(encoded)
147 | } else {
148 | const buffer = await value.arrayBuffer()
149 | const bytes = new Uint8Array(buffer)
150 | return toBitArray(bytes)
151 | }
152 | })
153 | const bytes = await Promise.all(blobs)
154 | return toList(bytes)
155 | }
156 |
157 | export function hasFormData(formData, key) {
158 | return formData.has(key)
159 | }
160 |
161 | export function keysFormData(formData) {
162 | const result = new Set()
163 | for (const key of formData.keys()) {
164 | result.add(key)
165 | }
166 | return toList([...result])
167 | }
168 |
--------------------------------------------------------------------------------
/src/gleam/fetch/form_data.gleam:
--------------------------------------------------------------------------------
1 | //// `FormData` are common structures on the web to send both string data, and
2 | //// blob. They're the default standard when using a `