├── .github └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── gleam.toml ├── manifest.toml ├── src ├── gleam │ └── http │ │ └── cowboy.gleam └── gleam_cowboy_native.erl └── test ├── gleam └── http │ └── cowboy_test.gleam └── gleam_cowboy_test.gleam /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: erlef/setup-beam@v1 15 | with: 16 | otp-version: "26.1" 17 | gleam-version: "1.6.2" 18 | rebar3-version: "3" 19 | - run: gleam test 20 | - run: gleam format --check src test 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.beam 2 | *.iml 3 | *.o 4 | *.plt 5 | *.swo 6 | *.swp 7 | *~ 8 | .erlang.cookie 9 | .eunit 10 | .idea 11 | ebin 12 | erl_crash.dump 13 | gen 14 | log 15 | logs 16 | build 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v1.0.0 - 2024-12-08 4 | 5 | - Updated for `gleam_stdlib` v0.45.0. 6 | 7 | ## v0.7.0 - 2024-03-20 8 | 9 | - Updated for Gleam v1.0.0. 10 | - Support dropped for Cowboy < v2.5 due to a breaking change. 11 | 12 | ## v0.6.0 - 2023-11-06 13 | 14 | - Updated for Gleam v0.32. 15 | 16 | ## v0.5.0 - 2022-09-10 17 | 18 | - Updated for `gleam_otp` v0.5.0. 19 | 20 | ## v0.4.0 - 2022-02-05 21 | 22 | - Updated for `gleam_http` v3.0.0. 23 | 24 | ## v0.3.0 - 2021-12-03 25 | 26 | - Converted to use the Gleam build tool. 27 | 28 | ## v0.2.3 - 2021-09-28 29 | 30 | - Updated for Gleam v0.17.0. 31 | 32 | ## v0.2.2 - 2020-11-19 33 | 34 | - Updated for Gleam OTP v0.1.3. 35 | 36 | ## v0.2.1 - 2020-11-07 37 | 38 | - Updated for Gleam OTP v0.1.3. 39 | 40 | ## v0.2.0 - 2020-11-05 41 | 42 | - The `start` function now returns a Gleam OTP `StartResult`. 43 | 44 | ## v0.1.2 - 2020-08-24 45 | 46 | - The set-cookie header is now correctly converted into the Cowboy expected 47 | format. 48 | - A link to the GitHub repo is included in the .app.src file. 49 | 50 | ## v0.1.1 - 2020-08-22 51 | 52 | - Include package description. 53 | - Include gleam_http in applications. 54 | 55 | ## v0.1.0 - 2020-08-22 56 | 57 | - Initial release. 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright {{copyright_year}}, {{author_name}} <{{author_email}}>. 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | http://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. 191 | 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gleam Cowboy! 🤠 2 | 3 | A Gleam HTTP service adapter for the [Cowboy][cowboy] web server. 4 | 5 | You may want to consider using the [Mist][mist] web server instead as it has 6 | better performance, features type safe websockets, and is written entirely in 7 | Gleam. It is also supported by the [Wisp][wisp] web framework. 8 | 9 | ## Installation 10 | 11 | ```sh 12 | gleam add gleam_cowboy 13 | ``` 14 | 15 | ```gleam 16 | import gleam/erlang/process 17 | import gleam/http/cowboy 18 | import gleam/http/response.{type Response} 19 | import gleam/http/request.{type Request} 20 | import gleam/bytes_tree.{type BytesTree} 21 | 22 | // Define a HTTP service 23 | // 24 | pub fn my_service(request: Request(t)) -> Response(BytesTree) { 25 | let body = bytes_tree.from_string("Hello, world!") 26 | 27 | response.new(200) 28 | |> response.prepend_header("made-with", "Gleam") 29 | |> response.set_body(body) 30 | } 31 | 32 | // Start it on port 3000! 33 | // 34 | pub fn main() { 35 | cowboy.start(my_service, on_port: 3000) 36 | process.sleep_forever() 37 | } 38 | ``` 39 | 40 | ## Limitations 41 | 42 | Cowboy does not support duplicate HTTP headers so any duplicates specified by 43 | the Gleam HTTP service will not be sent to the client. 44 | 45 | [cowboy]: https://github.com/ninenines/cowboy 46 | [mist]: https://github.com/rawhat/mist 47 | [wisp]: https://github.com/lpil/wisp 48 | -------------------------------------------------------------------------------- /gleam.toml: -------------------------------------------------------------------------------- 1 | name = "gleam_cowboy" 2 | version = "1.0.0" 3 | licences = ["Apache-2.0"] 4 | description = "Run Gleam HTTP services with the Cowboy web server" 5 | gleam = ">= 1.0.0" 6 | 7 | repository = { type = "github", user = "gleam-lang", repo = "cowboy" } 8 | links = [ 9 | { title = "Website", href = "https://gleam.run" }, 10 | { title = "Sponsor", href = "https://github.com/sponsors/lpil" }, 11 | ] 12 | 13 | [dependencies] 14 | gleam_stdlib = ">= 0.45.0 and < 2.0.0" 15 | gleam_http = "~> 3.0" 16 | gleam_otp = "~> 0.7" 17 | cowboy = "~> 2.5" 18 | gleam_erlang = "~> 0.22" 19 | 20 | [dev-dependencies] 21 | gleeunit = "~> 1.0" 22 | gleam_hackney = "~> 1.0" 23 | -------------------------------------------------------------------------------- /manifest.toml: -------------------------------------------------------------------------------- 1 | # This file was generated by Gleam 2 | # You typically do not need to edit this file 3 | 4 | packages = [ 5 | { name = "certifi", version = "2.12.0", build_tools = ["rebar3"], requirements = [], otp_app = "certifi", source = "hex", outer_checksum = "EE68D85DF22E554040CDB4BE100F33873AC6051387BAF6A8F6CE82272340FF1C" }, 6 | { name = "cowboy", version = "2.12.0", build_tools = ["make", "rebar3"], requirements = ["cowlib", "ranch"], otp_app = "cowboy", source = "hex", outer_checksum = "8A7ABE6D183372CEB21CAA2709BEC928AB2B72E18A3911AA1771639BEF82651E" }, 7 | { name = "cowlib", version = "2.13.0", build_tools = ["make", "rebar3"], requirements = [], otp_app = "cowlib", source = "hex", outer_checksum = "E1E1284DC3FC030A64B1AD0D8382AE7E99DA46C3246B815318A4B848873800A4" }, 8 | { name = "gleam_erlang", version = "0.33.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "A1D26B80F01901B59AABEE3475DD4C18D27D58FA5C897D922FCB9B099749C064" }, 9 | { name = "gleam_hackney", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib", "hackney"], otp_app = "gleam_hackney", source = "hex", outer_checksum = "A0F182181D116BACD85D961A6E1AA35D25091195BE6F38468ECC28956E2C1835" }, 10 | { name = "gleam_http", version = "3.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "A9EE0722106FCCAB8AD3BF9D0A3EFF92BFE8561D59B83BAE96EB0BE1938D4E0F" }, 11 | { name = "gleam_otp", version = "0.14.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "5A8CE8DBD01C29403390A7BD5C0A63D26F865C83173CF9708E6E827E53159C65" }, 12 | { name = "gleam_stdlib", version = "0.45.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "206FCE1A76974AECFC55AEBCD0217D59EDE4E408C016E2CFCCC8FF51278F186E" }, 13 | { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, 14 | { name = "hackney", version = "1.20.1", build_tools = ["rebar3"], requirements = ["certifi", "idna", "metrics", "mimerl", "parse_trans", "ssl_verify_fun", "unicode_util_compat"], otp_app = "hackney", source = "hex", outer_checksum = "FE9094E5F1A2A2C0A7D10918FEE36BFEC0EC2A979994CFF8CFE8058CD9AF38E3" }, 15 | { name = "idna", version = "6.1.1", build_tools = ["rebar3"], requirements = ["unicode_util_compat"], otp_app = "idna", source = "hex", outer_checksum = "92376EB7894412ED19AC475E4A86F7B413C1B9FBB5BD16DCCD57934157944CEA" }, 16 | { name = "metrics", version = "1.0.1", build_tools = ["rebar3"], requirements = [], otp_app = "metrics", source = "hex", outer_checksum = "69B09ADDDC4F74A40716AE54D140F93BEB0FB8978D8636EADED0C31B6F099F16" }, 17 | { name = "mimerl", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "mimerl", source = "hex", outer_checksum = "A1E15A50D1887217DE95F0B9B0793E32853F7C258A5CD227650889B38839FE9D" }, 18 | { name = "parse_trans", version = "3.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "parse_trans", source = "hex", outer_checksum = "620A406CE75DADA827B82E453C19CF06776BE266F5A67CFF34E1EF2CBB60E49A" }, 19 | { name = "ranch", version = "1.8.0", build_tools = ["make", "rebar3"], requirements = [], otp_app = "ranch", source = "hex", outer_checksum = "49FBCFD3682FAB1F5D109351B61257676DA1A2FDBE295904176D5E521A2DDFE5" }, 20 | { name = "ssl_verify_fun", version = "1.1.7", build_tools = ["mix", "rebar3", "make"], requirements = [], otp_app = "ssl_verify_fun", source = "hex", outer_checksum = "FE4C190E8F37401D30167C8C405EDA19469F34577987C76DDE613E838BBC67F8" }, 21 | { name = "unicode_util_compat", version = "0.7.0", build_tools = ["rebar3"], requirements = [], otp_app = "unicode_util_compat", source = "hex", outer_checksum = "25EEE6D67DF61960CF6A794239566599B09E17E668D3700247BC498638152521" }, 22 | ] 23 | 24 | [requirements] 25 | cowboy = { version = "~> 2.5" } 26 | gleam_erlang = { version = "~> 0.22" } 27 | gleam_hackney = { version = "~> 1.0" } 28 | gleam_http = { version = "~> 3.0" } 29 | gleam_otp = { version = "~> 0.7" } 30 | gleam_stdlib = { version = ">= 0.45.0 and < 2.0.0" } 31 | gleeunit = { version = "~> 1.0" } 32 | -------------------------------------------------------------------------------- /src/gleam/http/cowboy.gleam: -------------------------------------------------------------------------------- 1 | import gleam/bytes_tree.{type BytesTree} 2 | import gleam/dict.{type Dict} 3 | import gleam/dynamic.{type Dynamic} 4 | import gleam/erlang/process.{type Pid} 5 | import gleam/http.{type Header} 6 | import gleam/http/request.{type Request, Request} 7 | import gleam/http/response.{type Response} 8 | import gleam/list 9 | import gleam/option.{type Option, None, Some} 10 | import gleam/pair 11 | import gleam/result 12 | 13 | type CowboyRequest 14 | 15 | @external(erlang, "gleam_cowboy_native", "start_link") 16 | fn erlang_start_link( 17 | handler: fn(CowboyRequest) -> CowboyRequest, 18 | port: Int, 19 | ) -> Result(Pid, Dynamic) 20 | 21 | @external(erlang, "gleam_cowboy_native", "set_headers") 22 | fn erlang_set_headers( 23 | headers: Dict(String, Dynamic), 24 | request: CowboyRequest, 25 | ) -> CowboyRequest 26 | 27 | fn set_headers( 28 | headers: Dict(String, Dynamic), 29 | request: CowboyRequest, 30 | ) -> CowboyRequest { 31 | erlang_set_headers(headers, request) 32 | } 33 | 34 | @external(erlang, "cowboy_req", "reply") 35 | fn cowboy_reply( 36 | status: Int, 37 | headers: Dict(String, Dynamic), 38 | body: BytesTree, 39 | request: CowboyRequest, 40 | ) -> CowboyRequest 41 | 42 | @external(erlang, "cowboy_req", "method") 43 | fn erlang_get_method(request: CowboyRequest) -> Dynamic 44 | 45 | fn get_method(request) -> http.Method { 46 | request 47 | |> erlang_get_method 48 | |> http.method_from_dynamic 49 | |> result.unwrap(http.Get) 50 | } 51 | 52 | @external(erlang, "cowboy_req", "headers") 53 | fn erlang_get_headers(request: CowboyRequest) -> Dict(String, String) 54 | 55 | fn get_headers(request) -> List(http.Header) { 56 | request 57 | |> erlang_get_headers 58 | |> dict.to_list 59 | } 60 | 61 | @external(erlang, "gleam_cowboy_native", "read_entire_body") 62 | fn get_body(request: CowboyRequest) -> #(BitArray, CowboyRequest) 63 | 64 | @external(erlang, "cowboy_req", "scheme") 65 | fn erlang_get_scheme(request: CowboyRequest) -> String 66 | 67 | fn get_scheme(request) -> http.Scheme { 68 | request 69 | |> erlang_get_scheme 70 | |> http.scheme_from_string 71 | |> result.unwrap(http.Http) 72 | } 73 | 74 | @external(erlang, "cowboy_req", "qs") 75 | fn erlang_get_query(request: CowboyRequest) -> String 76 | 77 | fn get_query(request) -> Option(String) { 78 | case erlang_get_query(request) { 79 | "" -> None 80 | query -> Some(query) 81 | } 82 | } 83 | 84 | @external(erlang, "cowboy_req", "path") 85 | fn get_path(request: CowboyRequest) -> String 86 | 87 | @external(erlang, "cowboy_req", "host") 88 | fn get_host(request: CowboyRequest) -> String 89 | 90 | @external(erlang, "cowboy_req", "port") 91 | fn get_port(request: CowboyRequest) -> Int 92 | 93 | fn proplist_get_all(input: List(#(a, b)), key: a) -> List(b) { 94 | list.filter_map(input, fn(item) { 95 | case item { 96 | #(k, v) if k == key -> Ok(v) 97 | _ -> Error(Nil) 98 | } 99 | }) 100 | } 101 | 102 | // In cowboy all header values are strings except set-cookie, which is a 103 | // list. This list has a special-case in Cowboy so we need to set it 104 | // correctly. 105 | // https://github.com/gleam-lang/cowboy/issues/3 106 | fn cowboy_format_headers(headers: List(Header)) -> Dict(String, Dynamic) { 107 | let set_cookie_headers = proplist_get_all(headers, "set-cookie") 108 | headers 109 | |> list.map(pair.map_second(_, dynamic.from)) 110 | |> dict.from_list 111 | |> dict.insert("set-cookie", dynamic.from(set_cookie_headers)) 112 | } 113 | 114 | fn service_to_handler( 115 | service: fn(Request(BitArray)) -> Response(BytesTree), 116 | ) -> fn(CowboyRequest) -> CowboyRequest { 117 | fn(request) { 118 | let #(body, request) = get_body(request) 119 | let response = 120 | service(Request( 121 | body: body, 122 | headers: get_headers(request), 123 | host: get_host(request), 124 | method: get_method(request), 125 | path: get_path(request), 126 | port: Some(get_port(request)), 127 | query: get_query(request), 128 | scheme: get_scheme(request), 129 | )) 130 | let status = response.status 131 | 132 | let headers = cowboy_format_headers(response.headers) 133 | // We set headers directly on the CowboyRequest as we cannot set cookie headers 134 | // using cowboy_req:set_resp_headers as of 2.11.0. 135 | // https://github.com/ninenines/cowboy/pull/1624#issuecomment-1915324578 136 | let request = set_headers(headers, request) 137 | let body = response.body 138 | cowboy_reply(status, dict.new(), body, request) 139 | } 140 | } 141 | 142 | // TODO: document 143 | // TODO: test 144 | pub fn start( 145 | service: fn(Request(BitArray)) -> Response(BytesTree), 146 | on_port number: Int, 147 | ) -> Result(Pid, Dynamic) { 148 | service 149 | |> service_to_handler 150 | |> erlang_start_link(number) 151 | } 152 | -------------------------------------------------------------------------------- /src/gleam_cowboy_native.erl: -------------------------------------------------------------------------------- 1 | -module(gleam_cowboy_native). 2 | 3 | -export([init/2, start_link/2, read_entire_body/1, set_headers/2]). 4 | 5 | start_link(Handler, Port) -> 6 | RanchOptions = #{ 7 | max_connections => 16384, 8 | num_acceptors => 100, 9 | socket_opts => [{port, Port}] 10 | }, 11 | CowboyOptions = #{ 12 | env => #{dispatch => [{'_', [], [{'_', [], ?MODULE, Handler}]}]}, 13 | stream_handlers => [cowboy_stream_h] 14 | }, 15 | ranch_listener_sup:start_link( 16 | {gleam_cowboy, make_ref()}, 17 | ranch_tcp, RanchOptions, 18 | cowboy_clear, CowboyOptions 19 | ). 20 | 21 | init(Req, Handler) -> 22 | {ok, Handler(Req), Req}. 23 | 24 | read_entire_body(Req) -> 25 | read_entire_body([], Req). 26 | 27 | read_entire_body(Body, Req0) -> 28 | case cowboy_req:read_body(Req0) of 29 | {ok, Chunk, Req1} -> {list_to_binary([Body, Chunk]), Req1}; 30 | {more, Chunk, Req1} -> read_entire_body([Body, Chunk], Req1) 31 | end. 32 | 33 | set_headers(Headers, Req) -> 34 | Req#{resp_headers => Headers}. 35 | -------------------------------------------------------------------------------- /test/gleam/http/cowboy_test.gleam: -------------------------------------------------------------------------------- 1 | import gleam/bytes_tree.{type BytesTree} 2 | import gleam/hackney 3 | import gleam/http.{Get, Head, Http, Post} 4 | import gleam/http/cookie 5 | import gleam/http/cowboy 6 | import gleam/http/request.{type Request} 7 | import gleam/http/response.{type Response} 8 | import gleam/list 9 | 10 | pub fn echo_service(request: Request(BitArray)) -> Response(BytesTree) { 11 | let body = case request.body { 12 | <<>> -> bytes_tree.from_string("Default body") 13 | x -> bytes_tree.from_bit_array(x) 14 | } 15 | response.new(200) 16 | |> response.prepend_header("made-with", "Gleam") 17 | |> response.set_cookie("cookie_name", "cookie_value", cookie.defaults(Http)) 18 | |> response.set_body(body) 19 | } 20 | 21 | pub fn request_test() { 22 | // TODO: Assign these ports on random free ones aviable 23 | // TODO: Shut down server after test? 24 | let port = 3078 25 | let assert Ok(_) = cowboy.start(echo_service, on_port: port) 26 | 27 | let req = 28 | request.new() 29 | |> request.set_method(Get) 30 | |> request.set_host("0.0.0.0") 31 | |> request.set_scheme(http.Http) 32 | |> request.set_port(port) 33 | 34 | let assert Ok(resp) = hackney.send(req) 35 | let assert 200 = resp.status 36 | let assert Ok("Gleam") = response.get_header(resp, "made-with") 37 | let assert "Default body" = resp.body 38 | } 39 | 40 | pub fn get_request_does_not_discard_body_test() { 41 | let port = 3079 42 | let assert Ok(_) = cowboy.start(echo_service, on_port: port) 43 | 44 | let req = 45 | request.new() 46 | |> request.set_method(Get) 47 | |> request.set_host("0.0.0.0") 48 | |> request.set_scheme(http.Http) 49 | |> request.set_port(port) 50 | |> request.set_body("This does NOT get dropped") 51 | 52 | let assert Ok(resp) = hackney.send(req) 53 | let assert 200 = resp.status 54 | let assert Ok("Gleam") = response.get_header(resp, "made-with") 55 | let assert "This does NOT get dropped" = resp.body 56 | } 57 | 58 | pub fn head_request_discards_body_test() { 59 | let port = 3080 60 | let assert Ok(_) = cowboy.start(echo_service, on_port: port) 61 | 62 | let req = 63 | request.new() 64 | |> request.set_method(Head) 65 | |> request.set_host("0.0.0.0") 66 | |> request.set_scheme(http.Http) 67 | |> request.set_port(port) 68 | |> request.set_body("This gets dropped") 69 | 70 | let assert Ok(resp) = hackney.send(req) 71 | let assert 200 = resp.status 72 | let assert Ok("Gleam") = response.get_header(resp, "made-with") 73 | let assert "" = resp.body 74 | } 75 | 76 | pub fn body_is_echoed_on_post_test() { 77 | let port = 3081 78 | let assert Ok(_) = cowboy.start(echo_service, on_port: port) 79 | 80 | let req = 81 | request.new() 82 | |> request.set_method(Post) 83 | |> request.set_host("0.0.0.0") 84 | |> request.set_scheme(http.Http) 85 | |> request.set_port(port) 86 | |> request.set_body("Ping") 87 | 88 | let assert Ok(resp) = hackney.send(req) 89 | let assert 200 = resp.status 90 | let assert Ok("Gleam") = response.get_header(resp, "made-with") 91 | let assert "Ping" = resp.body 92 | } 93 | 94 | pub fn cookie_headers_are_handled_correctly_test() { 95 | let port = 3082 96 | let assert Ok(_) = cowboy.start(echo_service, on_port: port) 97 | 98 | let req = 99 | request.new() 100 | |> request.set_method(Get) 101 | |> request.set_host("0.0.0.0") 102 | |> request.set_scheme(http.Http) 103 | |> request.set_port(port) 104 | |> request.set_header("name", "value") 105 | 106 | let assert Ok(resp) = hackney.send(req) 107 | let assert 200 = resp.status 108 | let assert Ok("Gleam") = response.get_header(resp, "made-with") 109 | let cookies = response.get_cookies(resp) 110 | let assert True = list.contains(cookies, #("cookie_name", "cookie_value")) 111 | } 112 | -------------------------------------------------------------------------------- /test/gleam_cowboy_test.gleam: -------------------------------------------------------------------------------- 1 | import gleeunit 2 | 3 | pub fn main() { 4 | gleeunit.main() 5 | } 6 | --------------------------------------------------------------------------------