From b775d77222db588747d896b91d70b37441f169d5 Mon Sep 17 00:00:00 2001 From: Alexei Pastuchov Date: Sat, 20 Jun 2026 09:06:18 +0200 Subject: [PATCH 1/2] Fix HTTP protocol plugin always responding with HTTP/1.0 (issue #391) When a client sends an HTTP/1.1 request, the server was unconditionally writing HTTP/1.0 in the response status line. Track the client's protocol version in a new _http_11 member and echo it back in all response paths. HTTP/1.1 responses also include Connection: close since persistent connections are not yet implemented. Adds two regression tests that send HTTP/1.0 and HTTP/1.1 requests respectively and assert the matching version appears in the response. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Alexei Pastuchov --- .../plugins/protocol/http/protocol.cc | 44 ++++++++++++++----- tests/httpd_test.cc | 41 ++++++++++++++++- 2 files changed, 72 insertions(+), 13 deletions(-) diff --git a/libgearman-server/plugins/protocol/http/protocol.cc b/libgearman-server/plugins/protocol/http/protocol.cc index 99f5d913b..438237006 100644 --- a/libgearman-server/plugins/protocol/http/protocol.cc +++ b/libgearman-server/plugins/protocol/http/protocol.cc @@ -88,6 +88,7 @@ class HTTPtext : public gearmand::protocol::Context _sent_header(false), _background(false), _keep_alive(false), + _http_11(false), _http_response(gearmand::protocol::httpd::HTTP_OK) { } @@ -192,66 +193,82 @@ class HTTPtext : public gearmand::protocol::Context gearman_strcommand(packet->command), gearmand::protocol::httpd::response(response())); + const char *http_version= _http_11 ? "HTTP/1.1" : "HTTP/1.0"; + const char *connection_header= _http_11 ? "Connection: close\r\n" : ""; + size_t pack_size= 0; if (_sent_header == false) { if (response() != gearmand::protocol::httpd::HTTP_OK) { pack_size= (size_t)snprintf((char *)send_buffer, send_buffer_size, - "HTTP/1.0 %u %s\r\n" + "%s %u %s\r\n" "Server: Gearman/" PACKAGE_VERSION "\r\n" + "%s" "Content-Length: 0\r\n" "\r\n", - unsigned(response()), gearmand::protocol::httpd::response(response())); + http_version, + unsigned(response()), gearmand::protocol::httpd::response(response()), + connection_header); } else if (method() == gearmand::protocol::httpd::HEAD) { pack_size= (size_t)snprintf((char *)send_buffer, send_buffer_size, - "HTTP/1.0 200 OK\r\n" + "%s 200 OK\r\n" "X-Gearman-Job-Handle: %.*s\r\n" "Content-Length: %" PRIu64 "\r\n" "Server: Gearman/" PACKAGE_VERSION "\r\n" + "%s" "\r\n", + http_version, packet->command == GEARMAN_COMMAND_JOB_CREATED ? (int)packet->arg_size[0] : (int)packet->arg_size[0] - 1, (const char *)packet->arg[0], - (uint64_t)packet->data_size); + (uint64_t)packet->data_size, + connection_header); } else if (method() == gearmand::protocol::httpd::TRACE) { pack_size= (size_t)snprintf((char *)send_buffer, send_buffer_size, - "HTTP/1.0 200 OK\r\n" + "%s 200 OK\r\n" "Server: Gearman/" PACKAGE_VERSION "\r\n" "Connection: close\r\n" "Content-Type: message/http\r\n" - "\r\n"); + "\r\n", + http_version); } else if (method() == gearmand::protocol::httpd::POST) { pack_size= (size_t)snprintf((char *)send_buffer, send_buffer_size, - "HTTP/1.0 200 OK\r\n" + "%s 200 OK\r\n" "X-Gearman-Job-Handle: %.*s\r\n" "X-Gearman-Command: %s\r\n" "Content-Length: %" PRIu64 "\r\n" "Server: Gearman/" PACKAGE_VERSION "\r\n" + "%s" "\r\n", + http_version, packet->command == GEARMAN_COMMAND_JOB_CREATED ? int(packet->arg_size[0]) : int(packet->arg_size[0] - 1), - (const char *)packet->arg[0], // Job handle + (const char *)packet->arg[0], gearman_strcommand(packet->command), - (uint64_t)packet->data_size); // Content-length + (uint64_t)packet->data_size, + connection_header); } else { pack_size= (size_t)snprintf((char *)send_buffer, send_buffer_size, - "HTTP/1.0 200 OK\r\n" + "%s 200 OK\r\n" "X-Gearman-Job-Handle: %.*s\r\n" "X-Gearman-Command: %s\r\n" "Content-Length: %" PRIu64 "\r\n" "Server: Gearman/" PACKAGE_VERSION "\r\n" + "%s" "\r\n", + http_version, packet->command == GEARMAN_COMMAND_JOB_CREATED ? int(packet->arg_size[0]) : int(packet->arg_size[0] - 1), (const char *)packet->arg[0], gearman_strcommand(packet->command), - (uint64_t)packet->data_size); // Content-length + (uint64_t)packet->data_size, + connection_header); } _sent_header= true; @@ -396,10 +413,11 @@ class HTTPtext : public gearmand::protocol::Context } size_t version_size= request_size - size_t(version - request); - if (version_size == 8 and + if (version_size == 8 and strncmp(version, "HTTP/1.1", 8) == 0) { set_keep_alive(true); + _http_11= true; } else if (version_size == 8 and strncmp(version, "HTTP/1.0", 8) == 0) @@ -610,6 +628,7 @@ class HTTPtext : public gearmand::protocol::Context _sent_header= false; _background= false; _keep_alive= false; + _http_11= false; content.clear(); _method= gearmand::protocol::httpd::TRACE; _http_response= gearmand::protocol::httpd::HTTP_OK; @@ -643,6 +662,7 @@ class HTTPtext : public gearmand::protocol::Context bool _sent_header; bool _background; bool _keep_alive; + bool _http_11; std::string global_port; gearmand::protocol::httpd::response_t _http_response; std::vector content; diff --git a/tests/httpd_test.cc b/tests/httpd_test.cc index 3c52d55f3..0c2c85f1c 100644 --- a/tests/httpd_test.cc +++ b/tests/httpd_test.cc @@ -206,14 +206,53 @@ test_st HEAD_TESTS[] ={ { 0, 0, 0 } }; +static test_return_t regression_http_version_10_TEST(void *) +{ + Application curl("/usr/bin/curl"); + curl.add_option("--http1.0"); + curl.add_option("--include"); + curl.add_option("--silent"); + curl.add_option(host_url); + + ASSERT_EQ(Application::SUCCESS, curl.run()); + ASSERT_EQ(Application::SUCCESS, curl.join()); + + const char *output= curl.stdout_c_str(); + ASSERT_TRUE(output != NULL); + ASSERT_TRUE(strncmp(output, "HTTP/1.0", 8) == 0); + + return TEST_SUCCESS; +} + +static test_return_t regression_http_version_11_TEST(void *) +{ + Application curl("/usr/bin/curl"); + curl.add_option("--http1.1"); + curl.add_option("--include"); + curl.add_option("--silent"); + curl.add_option(host_url); + + ASSERT_EQ(Application::SUCCESS, curl.run()); + ASSERT_EQ(Application::SUCCESS, curl.join()); + + const char *output= curl.stdout_c_str(); + ASSERT_TRUE(output != NULL); + ASSERT_TRUE(strncmp(output, "HTTP/1.1", 8) == 0); + ASSERT_TRUE(strstr(output, "Connection: close") != NULL); + + return TEST_SUCCESS; +} + test_st regression_TESTS[] ={ + { "issue#391 HTTP/1.0 request gets HTTP/1.0 response", 0, regression_http_version_10_TEST }, + { "issue#391 HTTP/1.1 request gets HTTP/1.1 response", 0, regression_http_version_11_TEST }, { 0, 0, 0 } }; collection_st collection[] ={ { "curl", check_for_curl, 0, curl_TESTS }, { "GET", check_for_libcurl, 0, GET_TESTS }, - { "regression", 0, 0, regression_TESTS }, + { "regression", check_for_curl, 0, regression_TESTS }, { 0, 0, 0, 0 } }; From d8a3b8fde1bdc39dab0e38022592e4ddd3e864b7 Mon Sep 17 00:00:00 2001 From: Alexei Pastuchov Date: Sat, 20 Jun 2026 09:45:01 +0200 Subject: [PATCH 2/2] ci: install curl in Ubuntu and Alpine containers The curl-based httpd tests (including the new regression tests for issue #391) check for /usr/bin/curl and skip silently if absent. Ubuntu/Alpine container images don't have it by default, so those tests were never actually running in containerised CI builds. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Alexei Pastuchov --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4a4eef1c..e21944bcf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,7 +59,7 @@ jobs: sudo apt-get -o Acquire::Retries=3 update && apt-get -o Acquire::Retries=3 -y install git ;; alpine*) - apk add --no-cache make gcc g++ autoconf automake m4 bash git libtool file py3-sphinx util-linux-dev libuuid libevent-dev gperf boost-dev openssl-dev + apk add --no-cache make gcc g++ autoconf automake m4 bash git libtool file py3-sphinx util-linux-dev libuuid libevent-dev gperf boost-dev openssl-dev curl ;; *) ;; @@ -138,7 +138,7 @@ jobs: run: | case "${GHA_CONFIG_NAME}" in ubuntu*) - sudo apt-get update && sudo apt-get -o Acquire::Retries=3 install -y libboost-all-dev gperf libevent-dev uuid-dev sphinx-doc sphinx-common libhiredis-dev ${{ matrix.config.cc }} ${{ matrix.config.cxx }} + sudo apt-get update && sudo apt-get -o Acquire::Retries=3 install -y libboost-all-dev gperf libevent-dev uuid-dev sphinx-doc sphinx-common libhiredis-dev curl ${{ matrix.config.cc }} ${{ matrix.config.cxx }} ;; *) ;;