userver: Writing your first HTTP server
Loading...
Searching...
No Matches
Writing your first HTTP server

Before you start

Warning
Note that you can start with a ready to use opensourse service template to ease the development of your userver based services. The template already has a preconfigured CI, build and install scripts, testsuite and unit-tests setups.
service template is an implementation of an HTTP server application from this tutorial. You do not need to copy parts of code from this tutorial to service template as it already has them.

Make sure that you can compile and run core tests as described at Configure, Build and Install.

This sample provides some basic information on how to configure the service and how to setup testing. If you are eager to prototype and experiment, consider the Easy - library for single file prototyping instead.

Step by step guide

Typical HTTP server application in userver consists of the following parts:

  • Some application logic
  • HTTP handler component - component that ties application logic to HTTP handler.
  • Static config - startup config that does not change for the whole lifetime of an application.
  • int main() - startup code.

Let's write a simple server that responds with

  • "Hello, unknown user!\n" on every request to /hello URL without name argument;
  • "Hello, <name of the user>!\n" on every request to /hello URL with ?name=<name of the user>.

This sample also contains information on how to add unit tests, benchmarks and functional tests.

Application Logic

Our application logic is straightforward:

#include "say_hello.hpp"
#include <fmt/format.h>
namespace samples::hello {
std::string SayHelloTo(std::string_view name) {
if (name.empty()) {
name = "unknown user";
}
return fmt::format("Hello, {}!\n", name);
}
} // namespace samples::hello

The "say_hello.hpp" contains a signe function declaration, so that the implementation details are hidden and the header is lightweight to include:

#pragma once
#include <string>
#include <string_view>
namespace samples::hello {
std::string SayHelloTo(std::string_view name);
} // namespace samples::hello

HTTP handler component

HTTP handlers must derive from server::handlers::HttpHandlerBase and have a name, that is obtainable at compile time via kName variable and is obtainable at runtime via HandlerName().

The primary functionality of the handler should be located in HandleRequest function. Return value of this function is the HTTP response body. If an exception exc derived from server::handlers::CustomHandlerException is thrown from the function then the HTTP response code will be set to exc.GetCode() and exc.GetExternalErrorBody() would be used for HTTP response body. Otherwise if an exception exc derived from std::exception is thrown from the function then the HTTP response code will be set to 500.

#pragma once
// Note: this is for the purposes of tests/samples only
namespace samples::hello {
class HelloHandler final : public server::handlers::HttpHandlerBase {
public:
// `kName` is used as the component name in static config
static constexpr std::string_view kName = "handler-hello-sample";
// Component is valid after construction and is able to accept requests
using HttpHandlerBase::HttpHandlerBase;
std::string HandleRequest(server::http::HttpRequest& request, server::request::RequestContext&) const override;
};
} // namespace samples::hello
#include "hello_handler.hpp"
#include "say_hello.hpp"
namespace samples::hello {
std::string
HelloHandler::HandleRequest(server::http::HttpRequest& request, server::request::RequestContext& /*request_context*/)
const {
// Setting Content-Type: text/plain in a microservice response ensures
// the client interprets it as plain text, preventing misinterpretation or
// errors. Without this header, the client might assume a different format,
// such as JSON, HTML or XML, leading to potential processing issues or
// incorrect handling of the data.
request.GetHttpResponse().SetContentType(http::content_type::kTextPlain);
return samples::hello::SayHelloTo(request.GetArg("name"));
}
} // namespace samples::hello
Warning
Handle* functions are invoked concurrently on the same instance of the handler class. Use synchronization primitives or do not modify shared data in Handle*.

Static config

Now we have to configure the service by providing task_processors and default_task_processor options for the components::ManagerControllerComponent and configuring each component in components section:

# yaml
components_manager:
task_processors: # Task processor is an executor for coroutine tasks
main-task-processor: # Make a task processor for CPU-bound coroutine tasks.
worker_threads: 4 # Process tasks in 4 threads.
fs-task-processor: # Make a separate task processor for filesystem bound tasks.
worker_threads: 1
default_task_processor: main-task-processor # Task processor in which components start.
components: # Configuring components that were registered via component_list
server:
listener: # configuring the main listening socket...
port: 8080 # ...to listen on this port and...
task_processor: main-task-processor # ...process incoming requests on this task processor.
logging:
fs-task-processor: fs-task-processor
loggers:
default:
file_path: '@stderr'
level: debug
overflow_behavior: discard # Drop logs if the system is too busy to write them down.
handler-hello-sample: # Finally! Our handler.
path: /hello # Registering handler by URL '/hello'.
method: GET,POST # It will only reply to GET (HEAD) and POST requests.
task_processor: main-task-processor # Run it on CPU bound task processor

Note that all the components and handlers have their static options additionally described in docs.

int main()

Finally, we add our component to the components::MinimalServerComponentList(), and start the server with static configuration file passed from command line.

// Note: this is for the purposes of tests/samples only
#include <hello_handler.hpp>
int main(int argc, char* argv[]) {
auto component_list = components::MinimalServerComponentList().Append<samples::hello::HelloHandler>();
return utils::DaemonMain(argc, argv, component_list);
}

You can either pass argc, argv to utils::DaemonRun() to parse config yaml and config vars filepaths from arguments, or you may use embedded config file.

Embedded files

Sometimes it is handy to embed file(s) content into the binary to avoid additional filesystem reads. You may use it with userver_embed_file() cmake function. It generates cmake target which can be linked into your executable target.

Cmake part looks like the following:

userver_embed_file(${PROJECT_NAME}_config
NAME static_config_yaml
FILEPATH static_config.yaml
)
target_link_libraries(${PROJECT_NAME} ${PROJECT_NAME}_config)

C++ part looks simple - include the generated header and use utils::FindResource() function to get the embedded file contents:

int main(int, char*[]) {
auto component_list = components::MinimalServerComponentList().Append<samples::hello::HelloHandler>();
return utils::DaemonMain(components::InMemoryConfig{utils::FindResource("static_config_yaml")}, component_list);
}

CMake

The build scripts consist of the following parts:

  • Finding the installed userver package:
    find_package(userver COMPONENTS core REQUIRED)
    # Note: If userver was added via add_subdirectory(path/to/userver), then
    # the userver_setup_environment() should be called here.
  • Making an OBJECTS target with built sources that are used across unit tests, benchmarks and the service itself:
    add_library(${PROJECT_NAME}_objs OBJECT
    src/say_hello.hpp
    src/say_hello.cpp
    src/hello_handler.hpp
    src/hello_handler.cpp
    )
    target_link_libraries(${PROJECT_NAME}_objs userver::core)
    target_include_directories(${PROJECT_NAME}_objs PUBLIC src)
  • Building the service executable:
    add_executable(${PROJECT_NAME} main.cpp)
    target_link_libraries(${PROJECT_NAME} ${PROJECT_NAME}_objs)
  • Unit tests:
    add_executable(${PROJECT_NAME}-unittest unittests/say_hello_test.cpp)
    target_link_libraries(${PROJECT_NAME}-unittest ${PROJECT_NAME}_objs userver::utest)
    add_google_tests(${PROJECT_NAME}-unittest)
  • Benchmarks:
    add_executable(${PROJECT_NAME}_benchmark benchmarks/say_hello_bench.cpp)
    target_link_libraries(${PROJECT_NAME}_benchmark ${PROJECT_NAME}_objs userver::ubench)
    add_google_benchmark_tests(${PROJECT_NAME}_benchmark)
  • Finally, we add test directory as a directory with tests for testsuite:
    # Makes a virtualenv suitable for running pytests from `test` directory and integrates with `ctest`.
    userver_testsuite_add_simple()

Build and Run

To build the sample, execute the following build steps at the userver root directory:

mkdir build_release
cd build_release
cmake -DCMAKE_BUILD_TYPE=Release ..
make userver-samples-hello_service

The sample could be started by running make start-userver-samples-hello_service. The command would invoke testsuite start target that sets proper paths in the configuration files and starts the service.

To start the service manually run ./samples/hello_service/userver-samples-hello_service -c </path/to/static_config.yaml>.

Note
CMake doesn't copy static_config.yaml and file from samples directory into build directory.

Now you can send a request to your server from another terminal:

bash
$ curl 127.0.0.1:8080/hello
Hello, unknown user!

Unit tests

Unit tests could be implemented with one of UTEST macros in the following way:

#include "say_hello.hpp"
UTEST(SayHelloTo, Basic) {
EXPECT_EQ(samples::hello::SayHelloTo("Developer"), "Hello, Developer!\n");
EXPECT_EQ(samples::hello::SayHelloTo({}), "Hello, unknown user!\n");
}

Functional testing

Functional tests for the service could be implemented using the service_client fixture from pytest_userver.plugins.core in the following way:

async def test_hello_base(service_client):
response = await service_client.get('/hello')
assert response.status == 200
assert 'text/plain' in response.headers['Content-Type']
assert response.text == 'Hello, unknown user!\n'
assert 'X-RequestId' not in response.headers.keys(), 'Unexpected header'
response = await service_client.get('/hello', params={'name': 'userver'})
assert response.status == 200
assert 'text/plain' in response.headers['Content-Type']
assert response.text == 'Hello, userver!\n'

Do not forget to add the plugin in conftest.py:

# Adding a plugin from userver/testsuite/pytest_plugins/
pytest_plugins = ['pytest_userver.plugins.core']

Full sources

See the full example at: