AssignmentsWeb Client

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.rta

Lab 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!():

FunctionYour task
mainSelect the method, print request details, parse the URL, and connect request sending to response and error handling.
send_requestBuild and send a blocking HTTP request with the correct body and headers.
handle_responseCheck the HTTP status, then print text or formatted JSON.
handle_url_parse_errorMatch 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 optionMeaning
URLThe address to request, including its scheme, such as https://example.com.
-X METHOD or --method METHODChoose the method; the default is GET.
-d DATA or --data DATASupply form fields as name=value pairs separated by &.
--json JSONSupply a JSON value and select POST.
-h or --helpPrint 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: GET

The 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&note=a=b

contains 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 variantRequired standard-error line
RelativeUrlWithoutBaseError: The URL does not have a valid base protocol.
InvalidIpv4AddressError: The URL contains an invalid IPv4 address.
InvalidIpv6AddressError: The URL contains an invalid IPv6 address.
InvalidPortError: The URL contains an invalid port number.
Any other variantError parsing URL: followed by the error’s display text.

For example, this command is accepted in Rustrace’s F9 console:

cargo run -- www.example.com

It 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 test

The 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_text

Check 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.com

The 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_UTORID

This 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.