So I'm trying to ping an API with a post request, and the API is giving me a 400 status code. However it seems that the send function is forcing my program to exit early, rather than letting me handle the 400 code myself.
So in the source below, the dbg
statement is never hit. Instead, the program exits with the following message:
Program exited early with error: (HttpError (BadStatus 400))
Looking over the source from basic-cli, I don't see anything that obviously looks like it should do this. Anyone know anything about this what I'm doing wrong?
resp = Http.send! (buildRequest apiKey model messages)
dbg resp
output =
when resp |> Http.handleStringResponse is
Err err -> crash (Http.errorToString err)
Ok body -> body
Hi Ian,
I believe you are creating a Task chain with error accumulation that short circuits when an error is encountered. I'll see if I can modify this so it works as you intend.
I'm going to get diner but I can think you can follow this approach with Task.onErr
Should be
when ... is
Err (HttpError (BadStatus 404)) -> "Error 404"
Err err -> crash ...
Ok body -> body
Note that in your design the successful result of when ... is
is a Str, so this implies you will provide a valid string in the case of an 404 error
You might want to change that to return a Task Str ... instead, so that you can do some other action and not just return a value
Or, you could return a Result ( -> Err ExampleBadRequestTag
and -> Ok body
) instead, and handle the two options later in your code. What is more ergonomic depends on whether you can provide a replacement value when an error occurs right away
Yeah there are multiple ways to go. This is a working example in old-style Roc (I don't have much experience with ! yet):
app [main] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.11.0/SY4WWMhWQ9NvQgvIthcv15AUeA7rAIJHAHgiaSHGhdY.tar.br" }
import pf.Http
import pf.Task exposing [Task]
import pf.Stdout
import pf.Stderr
main =
request = {
method: Get,
headers: [],
url: "http://localhost:8080",
mimeType: "",
body: [],
timeout: TimeoutMilliseconds 5000,
}
sendT = Http.send request
Task.attempt sendT \sendResult ->
when sendResult is
Ok response ->
when response |> Http.handleStringResponse is
Ok body -> Stdout.line "Response body: $(body)."
# Body contained Invalid UTF-8
Err err -> Stderr.line (Http.errorToString err)
Err err -> Stderr.line (Inspect.toStr err)
I'll write a clean ! version tomorrow.
I'm also going to improve the error Program exited early with error:
.
Awesome, thanks for this @Anton . This is a great help! :thank_you:
One complaint that I have with the way Http errors are structured with the basic cli platform is that in the case of a BadStatus
error, there is no access provided to the response body. In order to debug my error from the API, I had to modify the basic-cli platform with to allow the BadStatus error to return the response body. Might be nice if the error was a tagged record which included both the error code # as well as the response body.
we can do that! Can you open an issue on the repo?
I sure can! I'd love to handle the PR too... Does this look good for the error types?
Error : [
BadRequest Str,
Timeout U64,
NetworkError,
BadStatus { code: U16, body: List U8 }, # changed from: BadStatus U16,
BadBody Str,
]
looks good to me! :raised_hands:
So I'm seeing one downside to using a List U8
for the body in the Error. There may be many reasons to keep the body in List U8
format that I'm not aware of, but I'll just call this out here.
In the case that the error goes unhandled and the program exits early and the platform attempts to print the error to the terminal, the List U8
is not particularly readable/helpful to the user. If the error is handled by the user they can of course handle the U8 List however they want.
But for example, you might get an output like this:
Program exited early with error: (HttpErr (BadStatus {body: [123, 34, 101, 114, 114, 111, 114, 34, 58, 123, 34, 109, 101, 115, 115, 97, 103, 101, 34, 58, 34, 77, 111, 100, 101, 108, 32, 109, 105, 115, 116, 114, 97, 108, 47, 109, 105, 115, 116, 114, 97, 108, 45, 115, 109, 97, 108, 108, 32, 105, 115, 32, 110, 111, 116, 32, 97, 118, 97, 105, 108, 97, 98, 108, 101, 34, 44, 34, 99, 111, 100, 101, 34, 58, 52, 48, 48, 125, 44, 34, 117, 115, 101, 114, 95, 105, 100, 34, 58, 34, 117, 115, 101, 114, 95, 50, 80, 111, 52, 88, 104, 97, 81, 78, 114, 73, 89, 115, 99, 88, 67, 89, 80, 57, 121, 113, 121, 118, 105, 49, 113, 78, 34, 125], code: 400}))
Maybe including the body as a Str
instead of a List U8
would be better for this reason?
That is indeed a shortcoming, can the conversion from List U8
to Str
fail in this case?
@Ian McLerran I've made the ! example here, I also asked for input for potential improvements.
There is Http.errorToString, it can be adjusted to try to convert the new List U8 to Utf8 string
@Anton I'm sure there are cases where it could, my thought would be to use Result.withDefault ""
. You'll still get the status code, but if the Utf8 conversion fails, you'll miss out on the body. Probably a rare edge case, but that risk is definitely a downside to string conversion.
@Karakatiza Only problem is Http.errorToString is not called when the platform does an Inspect.toStr err
from main.
We could make the body an opaque type and implement Inspect
on it in a way that first tries converting to string and defaults to printing the raw bytes if that fails.
One downside is that sometimes you’ll see the string version just because the result happens to be valid UTF8
As a dev I feel like adding Http.errorToString in a manual error handling is trivial, as well as decoding raw body bytes to utf8 if needed.
It may make sense to have a new Http.sendUtf8 return Str body, and keep Http.send that returns List U8. Trying to implicitly decode a bytestring as a utf8 string feels wrong.
Now that I think of it, I don't think having Inspect.toStr return entire body of Http error makes sense. It should either return just the error code, or maybe a preview of the body - maybe first ~50 bytes/characters. So that If you want to see the body in case of an error you are forced to add some manual handling, atleast Http.errorToString
Its Inspect, not Serialize after all (although I do not know the original philosophy of this builtin)
@Karakatiza I think you’re right on the money there… the inspect probably shouldn’t return the entire body. Maybe not even return the body at all? Definitely think it could be truncated. When handling the error manually, the body info will be there, but for implicit handling of the error, or any time when inspected, the status code (and *maybe a chunk of the body) is probably adequate.
Curious what others think…
I would indeed add a truncated body, I think it's good to remove friction for debugging where possible
I've submitted a PR for this here. Curious what folks think.
I'm in favour of truncating the List U8. I also think implicit str conversion and a separate function to receive strings from http responses are not the "Rocy" way to do things (having one way to do the same thing). We do have functions that do the same thing as 2 functions combined, but they are in place for performace reasons (Like List.WalkBackwards
Instead of List.reverse |> List.walk
), and we are trying to get rid of them by making the composition of functions just as performant with Iterator - like builtin list functions.
To be fair, there is no implicit Str conversion happening to the body value attached to BadStatus error tag. This is still a List U8, just wrapped in an opaque type. So the user has full control over how to handle those bytes.
The only Str conversion happening "implicitly" is in inspect, which is ultimately a glorified string conversion anyway.
Okay, sorry I didn't get it the first time. This sounds pretty nice then, since if the conversion fails with invalid utf8 bytes, the user probably doesn't expect the response body to be a valid string anyways. Yes, it would produce strings vs List U8 depending on the content, which isn't elegant, but I see it as a good solution in practice.
Last updated: Jul 06 2025 at 12:14 UTC