+220
-105
src/wisp.gleam
+220
-105
src/wisp.gleam
···
152
152
HttpResponse(Body)
153
153
154
154
/// Create an empty response with the given status code.
155
-
///
155
+
///
156
156
/// # Examples
157
-
///
157
+
///
158
158
/// ```gleam
159
159
/// response(200)
160
160
/// // -> Response(200, [], Empty)
161
161
/// ```
162
-
///
162
+
///
163
163
pub fn response(status: Int) -> Response {
164
164
HttpResponse(status, [], Empty)
165
165
}
166
166
167
167
/// Set the body of a response.
168
-
///
168
+
///
169
169
/// # Examples
170
-
///
170
+
///
171
171
/// ```gleam
172
172
/// response(200)
173
173
/// |> set_body(File("/tmp/myfile.txt"))
174
174
/// // -> Response(200, [], File("/tmp/myfile.txt"))
175
175
/// ```
176
-
///
176
+
///
177
177
pub fn set_body(response: Response, body: Body) -> Response {
178
178
response
179
179
|> response.set_body(body)
···
193
193
/// `set_body` function with the `File` body variant.
194
194
///
195
195
/// # Examples
196
-
///
196
+
///
197
197
/// ```gleam
198
198
/// response(200)
199
199
/// |> file_download(named: "myfile.txt", from: "/tmp/myfile.txt")
···
229
229
/// as this can result in cross-site scripting vulnerabilities.
230
230
///
231
231
/// # Examples
232
-
///
232
+
///
233
233
/// ```gleam
234
234
/// response(200)
235
235
/// |> file_download_from_memory(named: "myfile.txt", containing: "Hello, Joe!")
···
255
255
}
256
256
257
257
/// Create a HTML response.
258
-
///
258
+
///
259
259
/// The body is expected to be valid HTML, though this is not validated.
260
260
/// The `content-type` header will be set to `text/html`.
261
-
///
261
+
///
262
262
/// # Examples
263
-
///
263
+
///
264
264
/// ```gleam
265
265
/// let body = string_builder.from_string("<h1>Hello, Joe!</h1>")
266
266
/// html_response(body, 200)
267
267
/// // -> Response(200, [#("content-type", "text/html")], Text(body))
268
268
/// ```
269
-
///
269
+
///
270
270
pub fn html_response(html: StringBuilder, status: Int) -> Response {
271
271
HttpResponse(status, [#("content-type", "text/html")], Text(html))
272
272
}
273
273
274
274
/// Create a JSON response.
275
-
///
275
+
///
276
276
/// The body is expected to be valid JSON, though this is not validated.
277
277
/// The `content-type` header will be set to `application/json`.
278
-
///
278
+
///
279
279
/// # Examples
280
-
///
280
+
///
281
281
/// ```gleam
282
282
/// let body = string_builder.from_string("{\"name\": \"Joe\"}")
283
283
/// json_response(body, 200)
284
284
/// // -> Response(200, [#("content-type", "application/json")], Text(body))
285
285
/// ```
286
-
///
286
+
///
287
287
pub fn json_response(json: StringBuilder, status: Int) -> Response {
288
288
HttpResponse(status, [#("content-type", "application/json")], Text(json))
289
289
}
290
290
291
291
/// Set the body of a response to a given HTML document, and set the
292
292
/// `content-type` header to `text/html`.
293
-
///
293
+
///
294
294
/// The body is expected to be valid HTML, though this is not validated.
295
-
///
295
+
///
296
296
/// # Examples
297
-
///
297
+
///
298
298
/// ```gleam
299
299
/// let body = string_builder.from_string("<h1>Hello, Joe!</h1>")
300
300
/// response(201)
301
301
/// |> html_body(body)
302
302
/// // -> Response(201, [#("content-type", "text/html")], Text(body))
303
303
/// ```
304
-
///
304
+
///
305
305
pub fn html_body(response: Response, html: StringBuilder) -> Response {
306
306
response
307
307
|> response.set_body(Text(html))
···
310
310
311
311
/// Set the body of a response to a given JSON document, and set the
312
312
/// `content-type` header to `application/json`.
313
-
///
313
+
///
314
314
/// The body is expected to be valid JSON, though this is not validated.
315
-
///
315
+
///
316
316
/// # Examples
317
-
///
317
+
///
318
318
/// ```gleam
319
319
/// let body = string_builder.from_string("{\"name\": \"Joe\"}")
320
320
/// response(201)
321
321
/// |> json_body(body)
322
322
/// // -> Response(201, [#("content-type", "application/json")], Text(body))
323
323
/// ```
324
-
///
324
+
///
325
325
pub fn json_body(response: Response, json: StringBuilder) -> Response {
326
326
response
327
327
|> response.set_body(Text(json))
···
334
334
/// appropriate value for the format of the content.
335
335
///
336
336
/// # Examples
337
-
///
337
+
///
338
338
/// ```gleam
339
339
/// let body = string_builder.from_string("Hello, Joe!")
340
340
/// response(201)
341
341
/// |> string_builder_body(body)
342
342
/// // -> Response(201, [], Text(body))
343
343
/// ```
344
-
///
344
+
///
345
345
pub fn string_builder_body(
346
346
response: Response,
347
347
content: StringBuilder,
···
356
356
/// appropriate value for the format of the content.
357
357
///
358
358
/// # Examples
359
-
///
359
+
///
360
360
/// ```gleam
361
-
/// let body =
361
+
/// let body =
362
362
/// response(201)
363
363
/// |> string_body("Hello, Joe!")
364
364
/// // -> Response(
···
367
367
/// // Text(string_builder.from_string("Hello, Joe"))
368
368
/// // )
369
369
/// ```
370
-
///
370
+
///
371
371
pub fn string_body(response: Response, content: String) -> Response {
372
372
response
373
373
|> response.set_body(Text(string_builder.from_string(content)))
···
384
384
/// escape_html("<h1>Hello, Joe!</h1>")
385
385
/// // -> "<h1>Hello, Joe!</h1>"
386
386
/// ```
387
-
///
387
+
///
388
388
pub fn escape_html(content: String) -> String {
389
-
do_escape_html("", content)
389
+
let bits = <<content:utf8>>
390
+
let acc = do_escape_html(bits, 0, bits, [])
391
+
392
+
list.reverse(acc)
393
+
|> bit_array.concat
394
+
// We know the bit array produced by `do_escape_html` is still a valid utf8
395
+
// string so we coerce it without passing through the validation steps of
396
+
// `bit_array.to_string`.
397
+
|> coerce_bit_array_to_string
398
+
}
399
+
400
+
@external(erlang, "wisp_ffi", "coerce")
401
+
fn coerce_bit_array_to_string(bit_array: BitArray) -> String
402
+
403
+
// A possible way to escape chars would be to split the string into graphemes,
404
+
// traverse those one by one and accumulate them back into a string escaping
405
+
// ">", "<", etc. as we see them.
406
+
//
407
+
// However, we can be a lot more performant by working directly on the
408
+
// `BitArray` representing a Gleam UTF-8 String.
409
+
// This means that, instead of popping a grapheme at a time, we can work
410
+
// directly on BitArray slices: this has the big advantage of making sure we
411
+
// share as much as possible with the original string without having to build
412
+
// a new one from scratch.
413
+
//
414
+
@target(erlang)
415
+
fn do_escape_html(
416
+
bin: BitArray,
417
+
skip: Int,
418
+
original: BitArray,
419
+
acc: List(BitArray),
420
+
) -> List(BitArray) {
421
+
case bin {
422
+
// If we find a char to escape we just advance the `skip` counter so that
423
+
// it will be ignored in the following slice, then we append the escaped
424
+
// version to the accumulator.
425
+
<<"<":utf8, rest:bits>> -> {
426
+
let acc = [<<"<":utf8>>, ..acc]
427
+
do_escape_html(rest, skip + 1, original, acc)
428
+
}
429
+
430
+
<<">":utf8, rest:bits>> -> {
431
+
let acc = [<<">":utf8>>, ..acc]
432
+
do_escape_html(rest, skip + 1, original, acc)
433
+
}
434
+
435
+
<<"&":utf8, rest:bits>> -> {
436
+
let acc = [<<"&":utf8>>, ..acc]
437
+
do_escape_html(rest, skip + 1, original, acc)
438
+
}
439
+
440
+
// For any other bit that doesn't need to be escaped we go into an inner
441
+
// loop, consuming as much "non-escapable" chars as possible.
442
+
<<_char, rest:bits>> -> do_escape_html_regular(rest, skip, original, acc, 1)
443
+
444
+
<<>> -> acc
445
+
446
+
_ -> panic as "non byte aligned string, all strings should be byte aligned"
447
+
}
390
448
}
391
449
392
-
fn do_escape_html(escaped: String, content: String) -> String {
393
-
case string.pop_grapheme(content) {
394
-
Ok(#("<", xs)) -> do_escape_html(escaped <> "<", xs)
395
-
Ok(#(">", xs)) -> do_escape_html(escaped <> ">", xs)
396
-
Ok(#("&", xs)) -> do_escape_html(escaped <> "&", xs)
397
-
Ok(#(x, xs)) -> do_escape_html(escaped <> x, xs)
398
-
Error(_) -> escaped <> content
450
+
@target(erlang)
451
+
fn do_escape_html_regular(
452
+
bin: BitArray,
453
+
skip: Int,
454
+
original: BitArray,
455
+
acc: List(BitArray),
456
+
len: Int,
457
+
) -> List(BitArray) {
458
+
// Remember, if we're here it means we've found a char that doesn't need to be
459
+
// escaped, so what we want to do is advance the `len` counter until we reach
460
+
// a char that _does_ need to be escaped and take the slice going from
461
+
// `skip` with size `len`.
462
+
//
463
+
// Imagine we're escaping this string: "abc<def&ghi" and we've reached 'd':
464
+
// ```
465
+
// abc<def&ghi
466
+
// ^ `skip` points here
467
+
// ```
468
+
// We're going to be increasing `len` until we reach the '&':
469
+
// ```
470
+
// abc<def&ghi
471
+
// ^^^ len will be 3 when we reach the '&' that needs escaping
472
+
// ```
473
+
// So we take the slice corresponding to "def".
474
+
//
475
+
case bin {
476
+
// If we reach a char that has to be escaped we append the slice starting
477
+
// from `skip` with size `len` and the escaped char.
478
+
// This is what allows us to share as much of the original string as
479
+
// possible: we only allocate a new BitArray for the escaped chars,
480
+
// everything else is just a slice of the original String.
481
+
<<"<":utf8, rest:bits>> -> {
482
+
let assert Ok(slice) = bit_array.slice(original, skip, len)
483
+
let acc = [<<"<":utf8>>, slice, ..acc]
484
+
do_escape_html(rest, skip + len + 1, original, acc)
485
+
}
486
+
487
+
<<">":utf8, rest:bits>> -> {
488
+
let assert Ok(slice) = bit_array.slice(original, skip, len)
489
+
let acc = [<<">":utf8>>, slice, ..acc]
490
+
do_escape_html(rest, skip + len + 1, original, acc)
491
+
}
492
+
493
+
<<"&":utf8, rest:bits>> -> {
494
+
let assert Ok(slice) = bit_array.slice(original, skip, len)
495
+
let acc = [<<"&":utf8>>, slice, ..acc]
496
+
do_escape_html(rest, skip + len + 1, original, acc)
497
+
}
498
+
499
+
// If a char doesn't need escaping we keep increasing the length of the
500
+
// slice we're going to take.
501
+
<<_char, rest:bits>> ->
502
+
do_escape_html_regular(rest, skip, original, acc, len + 1)
503
+
504
+
<<>> ->
505
+
case skip {
506
+
0 -> [original]
507
+
_ -> {
508
+
let assert Ok(slice) = bit_array.slice(original, skip, len)
509
+
[slice, ..acc]
510
+
}
511
+
}
512
+
513
+
_ -> panic as "non byte aligned string, all strings should be byte aligned"
399
514
}
400
515
}
401
516
···
593
708
//
594
709
595
710
/// The connection to the client for a HTTP request.
596
-
///
711
+
///
597
712
/// The body of the request can be read from this connection using functions
598
713
/// such as `require_multipart_body`.
599
-
///
714
+
///
600
715
pub opaque type Connection {
601
716
Connection(
602
717
reader: Reader,
···
676
791
}
677
792
678
793
/// Get the maximum permitted size of a request body of the request in bytes.
679
-
///
794
+
///
680
795
pub fn get_max_body_size(request: Request) -> Int {
681
796
request.body.max_body_size
682
797
}
683
798
684
799
/// Set the secret key base used to sign cookies and other sensitive data.
685
-
///
800
+
///
686
801
/// This key must be at least 64 bytes long and should be kept secret. Anyone
687
802
/// with this secret will be able to manipulate signed cookies and other sensitive
688
803
/// data.
689
804
///
690
805
/// # Panics
691
-
///
806
+
///
692
807
/// This function will panic if the key is less than 64 bytes long.
693
808
///
694
809
pub fn set_secret_key_base(request: Request, key: String) -> Request {
···
701
816
}
702
817
703
818
/// Get the secret key base used to sign cookies and other sensitive data.
704
-
///
819
+
///
705
820
pub fn get_secret_key_base(request: Request) -> String {
706
821
request.body.secret_key_base
707
822
}
···
723
838
724
839
/// Get the maximum permitted total size of a files uploaded by a request in
725
840
/// bytes.
726
-
///
841
+
///
727
842
pub fn get_max_files_size(request: Request) -> Int {
728
843
request.body.max_files_size
729
844
}
···
743
858
744
859
/// Get the size limit for each chunk of the request body when read from the
745
860
/// client.
746
-
///
861
+
///
747
862
pub fn get_read_chunk_size(request: Request) -> Int {
748
863
request.body.read_chunk_size
749
864
}
750
865
751
866
/// A convenient alias for a HTTP request with a Wisp connection as the body.
752
-
///
867
+
///
753
868
pub type Request =
754
869
HttpRequest(Connection)
755
870
···
758
873
/// if the method is not correct.
759
874
///
760
875
/// # Examples
761
-
///
876
+
///
762
877
/// ```gleam
763
878
/// fn handle_request(request: Request) -> Response {
764
879
/// use <- wisp.require_method(request, http.Patch)
···
779
894
780
895
// TODO: re-export once Gleam has a syntax for that
781
896
/// Return the non-empty segments of a request path.
782
-
///
897
+
///
783
898
/// # Examples
784
899
///
785
900
/// ```gleam
···
793
908
794
909
// TODO: re-export once Gleam has a syntax for that
795
910
/// Set a given header to a given value, replacing any existing value.
796
-
///
911
+
///
797
912
/// # Examples
798
913
///
799
914
/// ```gleam
···
863
978
/// return an incorrect value, depending on the underlying web server. It is the
864
979
/// responsibility of the caller to cache the body if it is needed multiple
865
980
/// times.
866
-
///
981
+
///
867
982
/// If the body is larger than the `max_body_size` limit then an empty response
868
983
/// with status code 413: Entity too large will be returned to the client.
869
-
///
984
+
///
870
985
/// If the body is found not to be valid UTF-8 then an empty response with
871
986
/// status code 400: Bad request will be returned to the client.
872
-
///
987
+
///
873
988
/// # Examples
874
989
///
875
990
/// ```gleam
···
899
1014
/// return an incorrect value, depending on the underlying web server. It is the
900
1015
/// responsibility of the caller to cache the body if it is needed multiple
901
1016
/// times.
902
-
///
1017
+
///
903
1018
/// If the body is larger than the `max_body_size` limit then an empty response
904
1019
/// with status code 413: Entity too large will be returned to the client.
905
-
///
1020
+
///
906
1021
/// # Examples
907
1022
///
908
1023
/// ```gleam
···
925
1040
// TODO: don't always return entity to large. Other errors are possible, such as
926
1041
// network errors.
927
1042
/// Read the entire body of the request as a bit string.
928
-
///
1043
+
///
929
1044
/// You may instead wish to use the `require_bit_array_body` or the
930
1045
/// `require_string_body` middleware functions instead.
931
-
///
1046
+
///
932
1047
/// This function does not cache the body in any way, so if you call this
933
1048
/// function (or any other body reading function) more than once it may hang or
934
1049
/// return an incorrect value, depending on the underlying web server. It is the
935
1050
/// responsibility of the caller to cache the body if it is needed multiple
936
1051
/// times.
937
-
///
1052
+
///
938
1053
/// If the body is larger than the `max_body_size` limit then an empty response
939
1054
/// with status code 413: Entity too large will be returned to the client.
940
-
///
1055
+
///
941
1056
pub fn read_body_to_bitstring(request: Request) -> Result(BitArray, Nil) {
942
1057
let connection = request.body
943
1058
read_body_loop(
···
971
1086
/// A middleware which extracts form data from the body of a request that is
972
1087
/// encoded as either `application/x-www-form-urlencoded` or
973
1088
/// `multipart/form-data`.
974
-
///
1089
+
///
975
1090
/// Extracted fields are sorted into alphabetical order by key, so if you wish
976
1091
/// to use pattern matching the order can be relied upon.
977
-
///
1092
+
///
978
1093
/// ```gleam
979
1094
/// fn handle_request(request: Request) -> Response {
980
1095
/// use form <- wisp.require_form(request)
···
1003
1118
///
1004
1119
/// If the body cannot be parsed successfully then an empty response with status
1005
1120
/// code 400: Bad request will be returned to the client.
1006
-
///
1121
+
///
1007
1122
pub fn require_form(
1008
1123
request: Request,
1009
1124
next: fn(FormData) -> Response,
···
1030
1145
/// Unsupported media type if the header is not the expected value
1031
1146
///
1032
1147
/// # Examples
1033
-
///
1148
+
///
1034
1149
/// ```gleam
1035
1150
/// fn handle_request(request: Request) -> Response {
1036
1151
/// use <- wisp.require_content_type(request, "application/json")
···
1050
1165
}
1051
1166
1052
1167
/// A middleware which extracts JSON from the body of a request.
1053
-
///
1168
+
///
1054
1169
/// ```gleam
1055
1170
/// fn handle_request(request: Request) -> Response {
1056
1171
/// use json <- wisp.require_json(request)
···
1071
1186
///
1072
1187
/// If the body cannot be parsed successfully then an empty response with status
1073
1188
/// code 400: Bad request will be returned to the client.
1074
-
///
1189
+
///
1075
1190
pub fn require_json(request: Request, next: fn(Dynamic) -> Response) -> Response {
1076
1191
use <- require_content_type(request, "application/json")
1077
1192
use body <- require_string_body(request)
···
1305
1420
}
1306
1421
1307
1422
/// Data parsed from form sent in a request's body.
1308
-
///
1423
+
///
1309
1424
pub type FormData {
1310
1425
FormData(
1311
1426
/// String values of the form's fields.
···
1429
1544
///
1430
1545
/// The `under` parameter is the request path prefix that must match for the
1431
1546
/// file to be served.
1432
-
///
1547
+
///
1433
1548
/// | `under` | `from` | `request.path` | `file` |
1434
1549
/// |-----------|---------|--------------------|-------------------------|
1435
1550
/// | `/static` | `/data` | `/static/file.txt` | `/data/file.txt` |
···
1576
1691
/// > erlang.priv_directory("my_app")
1577
1692
/// // -> Ok("/some/location/my_app/priv")
1578
1693
/// ```
1579
-
///
1694
+
///
1580
1695
pub const priv_directory = erlang.priv_directory
1581
1696
1582
1697
//
···
1585
1700
1586
1701
/// Configure the Erlang logger, setting the minimum log level to `info`, to be
1587
1702
/// called when your application starts.
1588
-
///
1703
+
///
1589
1704
/// You may wish to use an alternative for this such as one provided by a more
1590
1705
/// sophisticated logging library.
1591
-
///
1706
+
///
1592
1707
/// In future this function may be extended to change the output format.
1593
-
///
1708
+
///
1594
1709
pub fn configure_logger() -> Nil {
1595
1710
logging.configure()
1596
1711
}
1597
1712
1598
1713
/// Log a message to the Erlang logger with the level of `emergency`.
1599
-
///
1714
+
///
1600
1715
/// See the [Erlang logger documentation][1] for more information.
1601
-
///
1716
+
///
1602
1717
/// [1]: https://www.erlang.org/doc/man/logger
1603
-
///
1718
+
///
1604
1719
pub fn log_emergency(message: String) -> Nil {
1605
1720
logging.log(logging.Emergency, message)
1606
1721
}
1607
1722
1608
1723
/// Log a message to the Erlang logger with the level of `alert`.
1609
-
///
1724
+
///
1610
1725
/// See the [Erlang logger documentation][1] for more information.
1611
-
///
1726
+
///
1612
1727
/// [1]: https://www.erlang.org/doc/man/logger
1613
-
///
1728
+
///
1614
1729
pub fn log_alert(message: String) -> Nil {
1615
1730
logging.log(logging.Alert, message)
1616
1731
}
1617
1732
1618
1733
/// Log a message to the Erlang logger with the level of `critical`.
1619
-
///
1734
+
///
1620
1735
/// See the [Erlang logger documentation][1] for more information.
1621
-
///
1736
+
///
1622
1737
/// [1]: https://www.erlang.org/doc/man/logger
1623
-
///
1738
+
///
1624
1739
pub fn log_critical(message: String) -> Nil {
1625
1740
logging.log(logging.Critical, message)
1626
1741
}
1627
1742
1628
1743
/// Log a message to the Erlang logger with the level of `error`.
1629
-
///
1744
+
///
1630
1745
/// See the [Erlang logger documentation][1] for more information.
1631
-
///
1746
+
///
1632
1747
/// [1]: https://www.erlang.org/doc/man/logger
1633
-
///
1748
+
///
1634
1749
pub fn log_error(message: String) -> Nil {
1635
1750
logging.log(logging.Error, message)
1636
1751
}
1637
1752
1638
1753
/// Log a message to the Erlang logger with the level of `warning`.
1639
-
///
1754
+
///
1640
1755
/// See the [Erlang logger documentation][1] for more information.
1641
-
///
1756
+
///
1642
1757
/// [1]: https://www.erlang.org/doc/man/logger
1643
-
///
1758
+
///
1644
1759
pub fn log_warning(message: String) -> Nil {
1645
1760
logging.log(logging.Warning, message)
1646
1761
}
1647
1762
1648
1763
/// Log a message to the Erlang logger with the level of `notice`.
1649
-
///
1764
+
///
1650
1765
/// See the [Erlang logger documentation][1] for more information.
1651
-
///
1766
+
///
1652
1767
/// [1]: https://www.erlang.org/doc/man/logger
1653
-
///
1768
+
///
1654
1769
pub fn log_notice(message: String) -> Nil {
1655
1770
logging.log(logging.Notice, message)
1656
1771
}
1657
1772
1658
1773
/// Log a message to the Erlang logger with the level of `info`.
1659
-
///
1774
+
///
1660
1775
/// See the [Erlang logger documentation][1] for more information.
1661
-
///
1776
+
///
1662
1777
/// [1]: https://www.erlang.org/doc/man/logger
1663
-
///
1778
+
///
1664
1779
pub fn log_info(message: String) -> Nil {
1665
1780
logging.log(logging.Info, message)
1666
1781
}
1667
1782
1668
1783
/// Log a message to the Erlang logger with the level of `debug`.
1669
-
///
1784
+
///
1670
1785
/// See the [Erlang logger documentation][1] for more information.
1671
-
///
1786
+
///
1672
1787
/// [1]: https://www.erlang.org/doc/man/logger
1673
-
///
1788
+
///
1674
1789
pub fn log_debug(message: String) -> Nil {
1675
1790
logging.log(logging.Debug, message)
1676
1791
}
···
1689
1804
1690
1805
/// Sign a message which can later be verified using the `verify_signed_message`
1691
1806
/// function to detect if the message has been tampered with.
1692
-
///
1807
+
///
1693
1808
/// Signed messages are not encrypted and can be read by anyone. They are not
1694
1809
/// suitable for storing sensitive information.
1695
-
///
1810
+
///
1696
1811
/// This function uses the secret key base from the request. If the secret
1697
1812
/// changes then the signature will no longer be verifiable.
1698
-
///
1813
+
///
1699
1814
pub fn sign_message(
1700
1815
request: Request,
1701
1816
message: BitArray,
···
1705
1820
}
1706
1821
1707
1822
/// Verify a signed message which was signed using the `sign_message` function.
1708
-
///
1823
+
///
1709
1824
/// Returns the content of the message if the signature is valid, otherwise
1710
1825
/// returns an error.
1711
-
///
1826
+
///
1712
1827
/// This function uses the secret key base from the request. If the secret
1713
1828
/// changes then the signature will no longer be verifiable.
1714
-
///
1829
+
///
1715
1830
pub fn verify_signed_message(
1716
1831
request: Request,
1717
1832
message: String,
···
1749
1864
/// wisp.ok()
1750
1865
/// |> wisp.set_cookie(request, "id", "123", wisp.PlainText, 60 * 60)
1751
1866
/// ```
1752
-
///
1867
+
///
1753
1868
/// Setting a signed cookie that the client can read but not modify:
1754
-
///
1869
+
///
1755
1870
/// ```gleam
1756
1871
/// wisp.ok()
1757
1872
/// |> wisp.set_cookie(request, "id", value, wisp.Signed, 60 * 60)
···
1821
1936
1822
1937
// TODO: chunk the body
1823
1938
/// Create a connection which will return the given body when read.
1824
-
///
1939
+
///
1825
1940
/// This function is intended for use in tests, though you probably want the
1826
1941
/// `wisp/testing` module instead.
1827
-
///
1942
+
///
1828
1943
pub fn create_canned_connection(
1829
1944
body: BitArray,
1830
1945
secret_key_base: String,