Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions core/functional_tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}-early-monitor-port-open)
add_subdirectory(dynamic_configs)
add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}-dynamic-configs)

add_subdirectory(error_pages)
add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}-error-pages)

add_subdirectory(graceful_shutdown)
add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}-graceful-shutdown)

Expand Down
6 changes: 6 additions & 0 deletions core/functional_tests/error_pages/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
project(userver-core-tests-error-pages CXX)

add_executable(${PROJECT_NAME} "main.cpp")
target_link_libraries(${PROJECT_NAME} userver::core)

userver_testsuite_add_simple()
1 change: 1 addition & 0 deletions core/functional_tests/error_pages/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<html><body>Nothing to see here.</body></html>
28 changes: 28 additions & 0 deletions core/functional_tests/error_pages/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#include <string>

#include <userver/components/minimal_server_component_list.hpp>
#include <userver/server/handlers/http_handler_base.hpp>
#include <userver/utest/using_namespace_userver.hpp>
#include <userver/utils/daemon_run.hpp>

namespace functional_tests {

/// The only handler of the service: everything else ends up on the failsafe
/// path, where the error pages are applied.
class HelloHandler final : public server::handlers::HttpHandlerBase {
public:
static constexpr std::string_view kName = "handler-hello";

using HttpHandlerBase::HttpHandlerBase;

std::string HandleRequestThrow(const server::http::HttpRequest&, server::request::RequestContext&) const override {
return "Hello world!\n";
}
};

} // namespace functional_tests

int main(int argc, char* argv[]) {
const auto component_list = components::MinimalServerComponentList().Append<functional_tests::HelloHandler>();
return utils::DaemonMain(argc, argv, component_list);
}
41 changes: 41 additions & 0 deletions core/functional_tests/error_pages/static_config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
components_manager:
task_processors:
main-task-processor:
worker_threads: 4

fs-task-processor:
worker_threads: 2

default_task_processor: main-task-processor

components:
logging:
fs-task-processor: fs-task-processor
loggers:
default:
file_path: '@stderr'
level: debug
overflow_behavior: discard

server:
listener:
port: 8080
task_processor: main-task-processor
# [error pages]
error-pages:
- statuses: [400, 404, 405]
status: 200
# A relative path is resolved against the working directory
# of the service; the tests make this one absolute.
body-path: index.html
headers:
Content-Type: text/html
X-Powered-By: userver
- statuses: [414]
body: 'the URI is too long'
# [error pages]

handler-hello:
path: /hello
method: GET
task_processor: main-task-processor
18 changes: 18 additions & 0 deletions core/functional_tests/error_pages/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import pytest

pytest_plugins = ['pytest_userver.plugins.core']

USERVER_CONFIG_HOOKS = ['error_pages_config_hook']


@pytest.fixture(scope='session')
def error_pages_config_hook(service_source_dir):
"""Makes the 'body-path' of the error page absolute."""

def _patch_config(config_yaml, config_vars):
listener = config_yaml['components_manager']['components']['server']['listener']
for page in listener['error-pages']:
if 'body-path' in page:
page['body-path'] = str(service_source_dir / page['body-path'])

return _patch_config
59 changes: 59 additions & 0 deletions core/functional_tests/error_pages/tests/test_error_pages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import socket

import pytest


@pytest.fixture(scope='session')
def error_page(service_source_dir) -> str:
return (service_source_dir / 'index.html').read_text()


async def test_handler_response_is_not_affected(service_client):
response = await service_client.get('/hello')
assert response.status == 200
assert response.text == 'Hello world!\n'
assert 'X-Powered-By' not in response.headers


async def test_unknown_path(service_client, error_page):
response = await service_client.get('/no/such/path')
assert response.status == 200
assert response.text == error_page
assert response.headers['Content-Type'] == 'text/html'
assert response.headers['X-Powered-By'] == 'userver'


async def test_method_not_allowed(service_client, error_page):
response = await service_client.post('/hello')
assert response.status == 200
assert response.text == error_page
assert response.headers['X-Powered-By'] == 'userver'


async def test_status_is_kept_if_not_configured(service_client):
# The URI is longer than the default 'max_url_size' of 8192 bytes.
response = await service_client.get('/' + 'x' * 9000)
assert response.status == 414
assert response.text == 'the URI is too long'


async def test_head_request_has_no_body(service_client, error_page):
response = await service_client.request('HEAD', '/no/such/path')
assert response.status == 200
assert response.content == b''
assert response.headers['Content-Length'] == str(len(error_page))


# A malformed request is rejected by the parser, before the routing takes
# place; both layers must end up on the same error page.
async def test_malformed_request(service_client, service_port, error_page):
with socket.create_connection(('localhost', service_port), timeout=10) as sock:
sock.sendall(b'FOOBAR / HTTP/1.1\r\nHost: localhost\r\n\r\n')
sock.shutdown(socket.SHUT_WR)
response = b''
while chunk := sock.recv(4096):
response += chunk

assert response.startswith(b'HTTP/1.1 200 OK\r\n'), response
assert response.endswith(error_page.encode()), response
assert b'\r\nX-Powered-By: userver\r\n' in response
22 changes: 22 additions & 0 deletions core/functional_tests/http2server/tests/test_low_level.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,28 @@ async def test_request_without_path_resets_stream(create_connection, service_cli
assert stream_id == 3


async def test_unknown_method_is_a_bad_request(create_connection, service_client):
await service_client.update_server_state()
async with create_connection() as (sock, conn):
# An unsupported ':method' makes the request malformed; it must not
# affect the rest of the connection (RFC 9113, 8.1.1).
headers = [(':method', 'TRACE')] + PSEUDO_HEADERS[1:]
stream_id = conn.get_next_available_stream_id()
conn.send_headers(stream_id, headers, end_stream=True)
await sock.sendall(conn.data_to_send())

events = []
while not any(isinstance(event, h2.events.ResponseReceived) for event in events):
events += await utils.send_and_receive(sock, conn)
response = next(event for event in events if isinstance(event, h2.events.ResponseReceived))
assert dict(response.headers)[b':status'] == b'400'

stream_id = conn.get_next_available_stream_id()
conn.send_headers(stream_id, DEFAULT_HEADERS, end_stream=True)
await sock.sendall(conn.data_to_send())
await utils.receive_simple_response(sock, conn)


async def test_single_reset_keeps_connection_usable(
create_connection,
monitor_client,
Expand Down
46 changes: 46 additions & 0 deletions core/src/server/component.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,52 @@ properties:
description: whether to write handler statistics
default: true

error-pages:
type: array
description: |
substitute responses for the errors that the server reports by itself, i.e. for the requests that
never reach a handler (unknown path, method not allowed, malformed request, throttling); an analog
of the nginx `error_page` directive. Responses produced by handlers are not affected.
default: '[] (the default server error responses are used)'
items:
type: object
description: substitute response for a set of error statuses
additionalProperties: false
required:
- statuses
properties:
statuses:
type: array
description: error statuses to substitute the response for
minItems: 1
items:
type: integer
description: error status
minimum: 400
maximum: 599
status:
type: integer
description: status to respond with instead of the original one
default: the original status is kept
minimum: 100
maximum: 599
body:
type: string
description: response body; mutually exclusive with 'body-path'
default: the server-generated body is kept
body-path:
type: string
description: path to a file with the response body, read at service start; mutually exclusive
with 'body'
headers:
type: object
description: headers to set on the response
default: '{}'
properties: {}
additionalProperties:
type: string
description: header value

connection:
type: object
description: connection options
Expand Down
Loading
Loading