We released the C++ REST SDK (codename “Casablanca”) as an open source project on CodePlex in Feb 2013. It enables writing modern, asynchronous C++ code that can connect with REST services.
Using the C++ REST SDK, you can create an HTTP client that can connect to HTTP server, send requests and handle responses. The following links are pre-requisites to get familiar with the C++ Rest SDK.
The C++ Rest HTTP Library offers an interesting feature which allows you to intercept your HTTP requests and responses before sending it out on the wire. This is useful for many scenarios and allows application developers to write custom handling of messages in one common place.
In this blog post, I will describe the intercepting technique in detail and will walk through few examples/scenarios where this can be used.
“Stages” of Http client message pipeline
Intercepting HTTP requests is achieved by defining “stages” on the http client before it is sent. These stages form the full pipeline for the http client. Say if a client defines 3 new stages for the request, this would mean that the actual request will be processed by each of the stages in order before being passed on to the last stage. The last stage (which is implemented internally by the http_client) will interact with lower-level communication layers to actually send the message on the network. When creating a client instance, an application may add pipeline stages in front of the already existing stages.
A stage is defined by adding a handler using http_client.add_handler() API. This API has two overloads,
- Pass in a std::function object as the handler: Following will ensure lambda “foo” gets called before the message is actually sent.
http_client.add_handler(foo)
- Implement a pipeline stage by deriving from the http_pipeline_stage class and pass it to the add_handler method.
The scenarios that we will discuss below will demonstrate how to use these overloads.
A handler can do 2 things:
- It can short-circuit the HTTP request and avoid sending the message over the network. The handler in this case should return a task representing the eventual response.
- Handler can do some extra processing of the message. For e.g. handler may update some counters, modify request/response headers etc. In this case, the handler should call the next stage.
One can similarly define “stages” to intercept the response received.
Scenario 1: Adding a stage to http_client request processing
Let us go through this with an example. Consider a client that uploads data to a service. However, this service has a limitation that it only accepts JSON data.
If a user sends xml or binary data in a PUT request, the service will reject this request. One way of avoiding this round-trip is by adding a step to the request processing pipeline, that checks the content-type and decides whether to continue with the request or not. In the below code-snippet, we have a content_handler lambda that does this for us:
This handler will check the content-type header and pass the request to the next stage only if the content-type is “application/json”. The second input parameter to the handler is the next_stage. For any other content-types, it fails the request immediately by replying with the BadRequest (400) HTTP code.
Note: In the below snippet, we are only checking for the standard JSON MIME type. Sites can use other JSON MIME types too.
auto content_handler = [](http_request request, std::shared_ptr<http_pipeline_stage> next_stage) -> pplx::task<http_response> { auto content_type = request.headers().content_type(); if (0 != content_type.compare(L”application/json”)) { // Short-circuit the HTTP request: construct a response object with status code = BadRequest // and return a task containing the response. http_response response; response.set_status_code(status_codes::BadRequest); return pplx::task_from_result(response); } else { // Content type is JSON, so call the next pipeline stage to send the request return next_stage->propagate(request); } };
http_client client(L“http://localhost:60009”); client.add_handler(content_handler);
client.request(methods::PUT, L“jsonentry1”, filebuf.create_istream(), L“application/json”) .then([](http_response response) { // Print the status code. std::wostringstream ss; ss << L“Server returned returned status code “ << response.status_code() << L‘.’ << std::endl; std::wcout << ss.str(); }); |
Scenario 2: Adding multiple stages to the client:
You can always add more than one handler, they will be executed in the order in which they were added.
Say in previous example, the client wants to add its version to every outgoing request. You can define a stage that can add a custom HTTP header named “AppVersion” with the client’s current version.
We will be using following overload of http_client.add_handler to achieve this.
http_client.add_handler(std::shared_ptr<http_pipeline_stage>)
The add_custom_headers stage extends http_pipeline_stage. During request processing, the http client runs this stage against the given request and passes onto the next stage. Each stage has a reference to the next stage available in the http_pipeline_stage::next_stage.
Implement the http_pipeline_stage::propagate()method to add your custom functionality. In the example below, this stageadds two new headers “AppVersion” and “ClientLocation” to the request message.
If the request was short circuited by the content_handler stage, the add_custom_headers stage will not be called. Only when the content_handler propagates the request to the next stage, will the headers be added.
// Pipeline stage used for adding custom headers class add_custom_headers : public http_pipeline_stage { public: virtual pplx::task<http_response> propagate(http_request request) { request.headers().add(L“AppVersion”, L“1.0”); request.headers().add(L“ClientLocation”, L”Redmond”));
return next_stage()->propagate(request); } };
http_client client(L“http://localhost:60009”); client.add_handler(content_handler); std::shared_ptr<http_pipeline_stage> custom_stage = std::make_shared<add_custom_headers>(); client.add_handler(custom_stage); |
Scenario 3: Intercepting HTTP response messages
In the above two illustrations, you saw how we can intercept the request pipeline. We can do the same with responses too. The client can add a new pipeline stage that will be executed before passing the response to the application. This stage can also modify the response received from the server.
The response_count_handler below increments a counter for each response received and also adds a new header to the response. The propagate()call returns a task of the response, its continuation can modify the response and return the updated one to the application.
Note that since multiple requests can be made simultaneously, accessing any data from within the handler must use synchronization. This will impact the performance and scalability of the application, which is a concern especially when implementing the handlers at the http_listener side. Hence, it is recommended to implement purely functional handlers: they should take a message, manipulate it and pass it to the next stage.
volatile long response_counter = 0; auto response_count_handler = [&response_counter](http_request request, std::shared_ptr<http_pipeline_stage> next_stage) -> pplx::task<http_response> { return next_stage->propagate(request).then([&response_counter](http_response resp) -> http_response { // Use synchronization primitives to access data from within the handlers. ::_InterlockedIncrement(&response_counter); resp.headers().add(L“ResponseHeader”, L“App”); return resp; }); };
http_client client(L”http://localhost:60009”); client.add_handler(response_count_handler); |
Scenario 4: Testing the HTTP client
This feature can be very useful for local testing as well. Instead of setting up a test server, you can add a stage that performs the server side validation at the client. These test hooks can reduce the test setup overhead significantly. For example: we can add a test handler that verifies a key and replies with the Forbidden (403) status code if the key is incorrect, thus performing a server side authentication step at the client itself.
auto test_auth_stage = [](http_request request, std::shared_ptr<http_pipeline_stage> next_stage) -> pplx::task<http_response> { if (is_valid_key(request.headers())) { return next_stage->propagate(request); } else { http_response response; response.set_status_code(status_codes::Forbidden); return pplx::task_from_result(response); } };
http_client client(L“https://www.cpprestsite.com”); client.add_handler(test_auth_stage); |
When can this come in handy?
These handlers are good for adding extra processing at the level of HTTP messages. Some examples of when these can be used:
- Logging purposes or to maintain counters for the number of requests and responses
- To add or modify the HTTP request or response headers.
- Perform some validation of the request before the actual processing. For example, you can check the authentication key and reject unauthorized requests. This way, you can avoid unnecessary roundtrips.
- Local testing: The handlers can act as test hooks and perform server side processing locally, without actually setting up a server.
Any feedback or comments are welcome either below or at our codeplex forum.
Kavya Kotacherry,
Visual C++ QA
0 comments