Web Client
In this programming assignment, you will complete a command-line HTTP client,
similar in spirit to the UNIX curl utility. You will use external Rust
crates to construct requests, send form and JSON data, inspect responses, and
report errors. Argument parsing and selected helpers are supplied. Your work
is to implement four functions, including the main workflow that connects
request construction, response handling, and error reporting.
The assignment is due Monday, November 16, 2026, at 11:59 p.m. Eastern time.
Getting Started
Write this assignment in Rustrace, the course editor required for
all assignments. Download lab3.rta, save it in a normal
local folder, and open it from a terminal:
rustrace work lab3.rtaLab 3 requires Rustrace 0.1.8 or later. If your installation refuses the
package, run rustrace update and try again. Rustrace creates
lab3.work containing a complete Cargo package, and
lab3.test-cases beside it containing two help-output checks.
The package supplies Cargo.toml and Cargo.lock with pinned
dependencies: reqwest for blocking HTTP requests, url for parsing URLs,
structopt for command-line arguments, and serde_json for JSON. It uses Rust
1.98.1 and the 2024 edition. The first build may download dependencies. Once
these are cached, the functional tests need no Internet connection: they
start small HTTP servers on your own machine.
On Linux, the supplied reqwest configuration also needs OpenSSL development
headers and pkg-config. If the first build reports missing OpenSSL files,
install these system prerequisites.
Read all of src/main.rs before editing it. The argument declarations,
the call to parse them, the JSON-key sorting helper, and the request-error
handler are supplied. Detailed comments explain each task and how the pieces
fit together. Implement the four functions marked with todo!():
| Function | Your task |
|---|---|
main | Select the method, print request details, parse the URL, and connect request sending to response and error handling. |
send_request | Build and send a blocking HTTP request with the correct body and headers. |
handle_response | Check the HTTP status, then print text or formatted JSON. |
handle_url_parse_error | Match URL parsing errors to the required messages. |
Each blank includes its inputs, required behavior, suggested steps, and common
pitfalls. Remove the placeholder bindings when implementing the functions and
add imports as needed. You may add Rust source files under src/. Keep
the supplied command-line interface and output format: the tests invoke your
compiled program.
Command-Line Interface
The program requires a URL and accepts these options:
| Argument or option | Meaning |
|---|---|
URL | The address to request, including its scheme, such as https://example.com. |
-X METHOD or --method METHOD | Choose the method; the default is GET. |
-d DATA or --data DATA | Supply form fields as name=value pairs separated by &. |
--json JSON | Supply a JSON value and select POST. |
-h or --help | Print the supplied help text and exit. |
This client supports GET and POST. Your main must select POST if
--json is present or the method string is exactly uppercase POST.
Every other method string selects GET. In particular, -d alone does not
change the method, and --json takes precedence over -X GET.
Before parsing the URL or sending a request, print these lines from main:
Requesting URL: <the original URL argument>
Method: GETThe second line is Method: POST when POST was selected. If -d was given,
a Data: <the original data argument> line follows. If --json was given,
a JSON: <the original JSON argument> line follows that. Both lines are
printed when both options are present, even though only the JSON body is
sent. Print these lines once in main, rather than repeating them in the
request or response handlers.
Connecting the Workflow
The first line of main calls Opt::from_args() for you. Keep this line:
StructOpt handles the command-line syntax and exits after displaying help,
which is why the help tests work even before the remaining code is complete.
Use the parsed opt value to implement the rest of the workflow.
Select the method and print the request details described above. Build a
HeaderMap, adding Content-Type: application/json when --json is present.
Borrow the optional strings while inspecting and printing them: later you
will pass ownership of opt.data and opt.json to send_request.
Parse the URL with Url::parse. On failure, call handle_url_parse_error
and exit with status 1 without attempting a request. On success, call
send_request with the parsed URL, chosen method, optional bodies, and headers.
Match its result: pass a successful response to handle_response; on error,
call the supplied handle_request_error and exit with status 1. Keep process
exit decisions in main, so the other functions have clear, separate jobs.
This workflow should connect your functions rather than duplicate their work.
For example, main decides which HTTP method to use, while send_request
chooses and encodes the body. An HTTP error status is still a received response;
let handle_response report it and then return normally.
Sending the Request
send_request receives a parsed Url, the selected method, optional form and
JSON strings, and a header map. Use reqwest::blocking::Client to build a
request. The function returns Result<Response, reqwest::Error>:
return the result of sending it so that main can handle success or failure.
Request-builder methods consume the builder and return an updated builder.
Keep that returned value when adding a body or headers. Apply the supplied
header map before sending. Send one request, using the method supplied by
main; do not invoke an external program such as the system curl command.
Form data. If data is present and json is absent, split the data string
at each &, then split each segment at its first =. Ignore segments without
=. Empty keys and values are allowed. Collect the pairs into a HashMap,
so the last value for a repeated key wins. For example:
name=Ada&topic=Rust&ignored¬e=a=bcontains name=Ada, topic=Rust, and note=a=b; the ignored segment is
skipped. Splitting only at the first = preserves a=b as the last value.
Pass the map to the request builder’s form operation. It encodes the fields
and sets the form content type. Do not decode + or % sequences beforehand:
the input values are literal strings. The order of fields in the encoded body
is not specified.
JSON data. If json is present, it takes precedence over data. Parse
it as a serde_json::Value, serialize that value to a compact JSON string,
and attach that string as the request body. Set Content-Type to
application/json. Parsing first validates the input; its original spacing
need not appear in the transmitted body.
For this assignment, invalid JSON must fail with a panic containing
Invalid JSON; you may use expect("Invalid JSON"). The tests do not
require a particular panic location or full panic message. Serialization of a
valid Value may use unwrap(). Do not send malformed JSON to the server.
With neither body option, leave the body unset. Return the result of send
directly. Do not call error_for_status: an HTTP 404 is a received response,
which handle_response must report, rather than a transport failure.
Handling the Response
For a successful HTTP status in the 200–299 range, read the response body
as text. You may use unwrap() on the body-read result for this
assignment. Inspect the status before reading the body, since reading consumes
the response.
Try parsing the text as serde_json::Value, regardless of its content-type
header. If parsing succeeds, call the supplied sort_json_keys helper and
serialize with serde_json::to_string_pretty. For a body {"z":2,"a":1},
print:
Response body (JSON with sorted keys):
{
"a": 1,
"z": 2
}The helper sorts keys recursively, including objects inside arrays, while preserving array-element order. Arrays and scalar values are valid JSON too.
If parsing fails, print the original text without trimming it. For a body
Hello, World! without a trailing newline, print:
Response body:
Hello, World!Both forms append a newline after the body. Preserve any existing newlines in a plain-text body, even when this produces an additional blank line.
For a non-success status, print the following to standard error, replacing 404 with the numeric status:
Error: Request failed with status code: 404.Do not print the error response body. Return normally: under this assignment’s control flow, an HTTP error response is reported but the program exits successfully. URL and transport failures instead exit with status 1.
Reporting Errors
Implement handle_url_parse_error by matching the error enum, not its display
text. Print the appropriate message to standard error. The function only
prints; your main must exit with status 1 afterward.
| URL error variant | Required standard-error line |
|---|---|
RelativeUrlWithoutBase | Error: The URL does not have a valid base protocol. |
InvalidIpv4Address | Error: The URL contains an invalid IPv4 address. |
InvalidIpv6Address | Error: The URL contains an invalid IPv6 address. |
InvalidPort | Error: The URL contains an invalid port number. |
| Any other variant | Error parsing URL: followed by the error’s display text. |
For example, this command is accepted in Rustrace’s F9 console:
cargo run -- www.example.comIt prints the request URL and method, then reports the missing base protocol
on standard error. Other examples are https://[...1] (invalid IPv6),
http://256.256.256.256 (invalid IPv4), and http://127.0.0.1:65536 (invalid
port).
The supplied handle_request_error handles failures after URL parsing. A
request-builder error prints the base-protocol message. A connection failure
prints:
Error: Unable to connect to the server. Perhaps the network is offline or the server hostname cannot be resolved.Other request errors print Error sending request: followed by the error’s
display text. Preserve this supplied handler and make your main exit with
status 1 after it returns.
Testing in Rustrace
The main functional checks are the ten public Rust tests in
tests/public_cli.rs. They invoke the compiled client and check its
output, exit status, and, where relevant, the HTTP request it sends to a local
server. Comments explain the test setup and expected behavior. You do not need
to start a server yourself.
Choose Test in the F7 menu, or enter this in the F9 console:
cargo testThe unfinished starter compiles, but its functional tests fail until the missing functions are implemented. To run one test while debugging:
cargo test get_prints_plain_textCheck that the reported count is one: a misspelled filter can match zero tests.
The tests use loopback addresses and temporary ports, without relying on public
APIs, DNS, or remote website content. They supply JSON and form arguments
directly to the child process, preserving quotes, spaces, and & characters.
Rustrace’s F9 console is not a shell. It splits arguments at spaces and rejects
quotes, &, and other shell syntax. Use the Rust tests for JSON objects and
multi-field form data instead of pasting shell-quoted commands into the console.
A simple request such as the following is accepted, but the response depends
on the external site and is not a grading check:
cargo run -- https://example.comThe F4 picker contains only two help-output smoke checks, for -h and
--help. Both already pass in the unfinished starter. They verify that the
supplied application starts, not that your HTTP implementation is complete,
and they are not graded. The Rust tests provide the local HTTP servers that
F4 cannot start.
Use Check and Clippy in the F7 menu as you work. The starter is warning-free, and your completed program should remain warning-free. Keep the supplied manifest and lockfile so your dependency versions match the tests.
Notes on AI Tools
This assignment is designed for manual work without AI assistance, and you should not use AI tools. One objective of the course is to learn Rust by writing and debugging code yourself; using AI changes that learning experience and defeats the purpose of the exercise.
You must write the assignment in Rustrace. It records how your program comes together and accepts only text that you type or copy within the editor itself. See the Rustrace guide for installation and usage instructions.
Submission
Submit by Monday, November 16, 2026, at 11:59 p.m. Eastern time. Late submissions will not be accepted.
When you are done, quit Rustrace and create your submission from a terminal, using your UTORid:
rustrace submit lab3.work --student-id YOUR_UTORIDThis finalizes the workspace and writes a ZIP file beside lab3.work.
Upload that ZIP to Quercus under Assignment 3. Rustrace does not upload it
for you. The ZIP contains everything needed for marking; do not add, remove,
or rename files. If you need another attempt after finalizing, follow the
revision procedure in the Rustrace guide. Keep the workspace until
your grade is released.
Marking
The automarker runs 20 equally weighted tests: the 10 public tests in your crate and 10 private tests kept by the teaching team. Each is worth 5 points. After your source has been verified, the grader restores authoritative copies of the public tests, Cargo manifest, and lockfile, and adds the private suite. Changing your copy of the tests does not affect marking.
Tests cover request methods, form encoding, JSON bodies, option precedence, text and JSON responses, nested JSON, HTTP status errors, malformed URLs, invalid JSON, and connection failures. Output is compared exactly where this handout specifies it. Form-field order and full panic text are not prescribed. The two F4 help checks are separate from the 20 marked tests.