From 361872cdcb35d814cda461fd4be796384a8d9b31 Mon Sep 17 00:00:00 2001 From: Ian Thomas Date: Fri, 27 Mar 2026 11:55:31 +0000 Subject: [PATCH 1/4] Add user-configurable timeout to wasm http requests --- CMakeLists.txt | 3 +++ src/wasm/constants.hpp | 11 +++++++++++ src/wasm/stream.cpp | 3 +++ src/wasm/subtransport.cpp | 4 +++- src/wasm/subtransport.hpp | 3 ++- src/wasm/utils.cpp | 38 ++++++++++++++++++++++++++++++++++++++ src/wasm/utils.hpp | 8 ++++++++ 7 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 src/wasm/constants.hpp create mode 100644 src/wasm/utils.cpp create mode 100644 src/wasm/utils.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ef6fb9b..a045f1e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -100,6 +100,7 @@ set(GIT2CPP_SRC ${GIT2CPP_SOURCE_DIR}/utils/progress.hpp ${GIT2CPP_SOURCE_DIR}/utils/terminal_pager.cpp ${GIT2CPP_SOURCE_DIR}/utils/terminal_pager.hpp + ${GIT2CPP_SOURCE_DIR}/wasm/constants.hpp ${GIT2CPP_SOURCE_DIR}/wasm/libgit2_internals.cpp ${GIT2CPP_SOURCE_DIR}/wasm/libgit2_internals.hpp ${GIT2CPP_SOURCE_DIR}/wasm/response.cpp @@ -112,6 +113,8 @@ set(GIT2CPP_SRC ${GIT2CPP_SOURCE_DIR}/wasm/subtransport.hpp ${GIT2CPP_SOURCE_DIR}/wasm/transport.cpp ${GIT2CPP_SOURCE_DIR}/wasm/transport.hpp + ${GIT2CPP_SOURCE_DIR}/wasm/utils.cpp + ${GIT2CPP_SOURCE_DIR}/wasm/utils.hpp ${GIT2CPP_SOURCE_DIR}/wrapper/annotated_commit_wrapper.cpp ${GIT2CPP_SOURCE_DIR}/wrapper/annotated_commit_wrapper.hpp ${GIT2CPP_SOURCE_DIR}/wrapper/branch_wrapper.cpp diff --git a/src/wasm/constants.hpp b/src/wasm/constants.hpp new file mode 100644 index 0000000..2159538 --- /dev/null +++ b/src/wasm/constants.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include + +// Constants used in wasm transport layer. +// Exposed to non-emscripten builds so that they can be used in help. + +// Environment variable for http transport timeout. +// Must be a positive number as a timeout of 0 will block forever. +inline constexpr std::string_view WASM_HTTP_TRANSPORT_TIMEOUT_NAME = "GIT_HTTP_TIMEOUT"; +inline constexpr unsigned int WASM_HTTP_TRANSPORT_TIMEOUT_DEFAULT = 10; diff --git a/src/wasm/stream.cpp b/src/wasm/stream.cpp index afc039e..d17af21 100644 --- a/src/wasm/stream.cpp +++ b/src/wasm/stream.cpp @@ -51,6 +51,7 @@ EM_JS( const char* method, const char* content_type_header, const char* authorization_header, + unsigned long request_timeout_ms, size_t buffer_size), { const url_js = UTF8ToString(url); @@ -76,6 +77,7 @@ EM_JS( { xhr.setRequestHeader("x-runtime-token", "{#{RUNTIME_TOKEN}#}"); } + xhr.timeout = request_timeout_ms; // Cache request info on JavaScript side so that it is available in subsequent calls // without having to pass it back and forth to/from C++. @@ -285,6 +287,7 @@ static int create_request(wasm_http_stream* stream, std::string_view content_hea name_for_method(stream->m_service.m_method).c_str(), content_header.data(), stream->m_subtransport->m_authorization_header.c_str(), + stream->m_subtransport->m_request_timeout_ms, EMFORGE_BUFSIZE ); return stream->m_request_index; diff --git a/src/wasm/subtransport.cpp b/src/wasm/subtransport.cpp index bdf0202..609f73c 100644 --- a/src/wasm/subtransport.cpp +++ b/src/wasm/subtransport.cpp @@ -2,15 +2,16 @@ # include "subtransport.hpp" +# include # include # include -# include # include # include # include "libgit2_internals.hpp" # include "stream.hpp" +# include "utils.hpp" // C functions. @@ -93,6 +94,7 @@ int create_wasm_http_subtransport(git_smart_subtransport** out, git_transport* o subtransport->m_owner = owner; subtransport->m_base_url = ""; subtransport->m_credential = nullptr; + subtransport->m_request_timeout_ms = get_request_timeout_ms(); *out = &subtransport->m_parent; return 0; diff --git a/src/wasm/subtransport.hpp b/src/wasm/subtransport.hpp index 1bb3bb6..6dd58df 100644 --- a/src/wasm/subtransport.hpp +++ b/src/wasm/subtransport.hpp @@ -17,7 +17,8 @@ struct wasm_http_subtransport // Data stored for reuse on other streams of this transport: std::string m_base_url; std::string m_authorization_header; - git_credential* m_credential; // libgit2 creates this, we are responsible for deleting it. + git_credential* m_credential; // libgit2 creates this, we are responsible for deleting it. + unsigned long m_request_timeout_ms; // Timeout for http(s) requests in milliseconds. }; // git_smart_subtransport_cb diff --git a/src/wasm/utils.cpp b/src/wasm/utils.cpp new file mode 100644 index 0000000..390b3f2 --- /dev/null +++ b/src/wasm/utils.cpp @@ -0,0 +1,38 @@ +#ifdef EMSCRIPTEN + +# include "utils.hpp" + +# include + +# include "constants.hpp" + +unsigned long get_request_timeout_ms() +{ + double timeout_seconds = WASM_HTTP_TRANSPORT_TIMEOUT_DEFAULT; + auto env_var = std::getenv(WASM_HTTP_TRANSPORT_TIMEOUT_NAME.data()); + if (env_var != nullptr) + { + try + { + auto value = std::stod(env_var); + if (value <= 0) + { + throw std::runtime_error("negative or zero"); + } + timeout_seconds = value; + } + catch (std::exception& e) + { + // Catch failures from (1) stod and (2) timeout <= 0. + // Print warning and use default value. + std::cout << termcolor::yellow << "Warning: environment variable " + << WASM_HTTP_TRANSPORT_TIMEOUT_NAME + << " must be a positive number of seconds, using default value of " + << WASM_HTTP_TRANSPORT_TIMEOUT_DEFAULT << " seconds instead." << termcolor::reset + << std::endl; + } + } + return 1000 * timeout_seconds; +} + +#endif // EMSCRIPTEN diff --git a/src/wasm/utils.hpp b/src/wasm/utils.hpp new file mode 100644 index 0000000..2325479 --- /dev/null +++ b/src/wasm/utils.hpp @@ -0,0 +1,8 @@ +#pragma once + +#ifdef EMSCRIPTEN + +// Get wasm http request timeout in milliseconds from environment variable or default value. +unsigned long get_request_timeout_ms(); + +#endif // EMSCRIPTEN From 84c2a7c46ee45bcd30794fa7ecc11b8c93ebb228 Mon Sep 17 00:00:00 2001 From: Ian Thomas Date: Thu, 30 Apr 2026 14:30:01 +0100 Subject: [PATCH 2/4] Timeout error message --- src/wasm/constants.hpp | 2 +- src/wasm/stream.cpp | 17 ++++++++++++++--- src/wasm/utils.cpp | 8 ++++---- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/wasm/constants.hpp b/src/wasm/constants.hpp index 2159538..d6f9dee 100644 --- a/src/wasm/constants.hpp +++ b/src/wasm/constants.hpp @@ -8,4 +8,4 @@ // Environment variable for http transport timeout. // Must be a positive number as a timeout of 0 will block forever. inline constexpr std::string_view WASM_HTTP_TRANSPORT_TIMEOUT_NAME = "GIT_HTTP_TIMEOUT"; -inline constexpr unsigned int WASM_HTTP_TRANSPORT_TIMEOUT_DEFAULT = 10; +inline constexpr unsigned int WASM_HTTP_TRANSPORT_TIMEOUT_DEFAULT_S = 10; diff --git a/src/wasm/stream.cpp b/src/wasm/stream.cpp index d17af21..65cef68 100644 --- a/src/wasm/stream.cpp +++ b/src/wasm/stream.cpp @@ -8,6 +8,7 @@ # include # include "../utils/common.hpp" +# include "constants.hpp" # include "response.hpp" // Buffer size used in transport_smart, hardcoded in libgit2. @@ -106,7 +107,7 @@ EM_JS( // clang-format off Module["git2cpp_js_error"] = { name: err.name ?? "", message : err.message ?? "" }; // clang-format on - console.error(err); + (err.name == "TimeoutError" ? console.warn : console.error)(err); return -1; } } @@ -208,7 +209,7 @@ EM_JS( // clang-format off Module["git2cpp_js_error"] = { name: err.name ?? "", message : err.message ?? "" }; // clang-format on - console.error(err); + (err.name == "TimeoutError" ? console.warn : console.error)(err); return -1; } } @@ -245,7 +246,7 @@ EM_JS(size_t, js_write, (int request_index, const char* buffer, size_t buffer_si // clang-format off Module["git2cpp_js_error"] = { name: err.name ?? "", message : err.message ?? "" }; // clang-format on - console.error(err); + (err.name == "TimeoutError" ? console.warn : console.error)(err); return -1; } }); @@ -273,6 +274,16 @@ static void convert_js_to_git_error(wasm_http_stream* stream) stream->m_unconverted_url.c_str() ); } + else if (std::string_view(error_str).starts_with("TimeoutError:")) + { + git_error_set( + GIT_ERROR_HTTP, + "network request timed out connecting to %s. You can set a longer timeout in seconds using the environment variable %s, the default value is %u seconds.", + stream->m_unconverted_url.c_str(), + WASM_HTTP_TRANSPORT_TIMEOUT_NAME.data(), + WASM_HTTP_TRANSPORT_TIMEOUT_DEFAULT_S + ); + } else { git_error_set(GIT_ERROR_HTTP, "%s", error_str); diff --git a/src/wasm/utils.cpp b/src/wasm/utils.cpp index 390b3f2..7cc34e5 100644 --- a/src/wasm/utils.cpp +++ b/src/wasm/utils.cpp @@ -8,16 +8,16 @@ unsigned long get_request_timeout_ms() { - double timeout_seconds = WASM_HTTP_TRANSPORT_TIMEOUT_DEFAULT; + double timeout_seconds = WASM_HTTP_TRANSPORT_TIMEOUT_DEFAULT_S; auto env_var = std::getenv(WASM_HTTP_TRANSPORT_TIMEOUT_NAME.data()); if (env_var != nullptr) { try { auto value = std::stod(env_var); - if (value <= 0) + if (value < 1e-3) // Must be at least 1 ms. { - throw std::runtime_error("negative or zero"); + throw std::runtime_error(""); // Caught below. } timeout_seconds = value; } @@ -28,7 +28,7 @@ unsigned long get_request_timeout_ms() std::cout << termcolor::yellow << "Warning: environment variable " << WASM_HTTP_TRANSPORT_TIMEOUT_NAME << " must be a positive number of seconds, using default value of " - << WASM_HTTP_TRANSPORT_TIMEOUT_DEFAULT << " seconds instead." << termcolor::reset + << WASM_HTTP_TRANSPORT_TIMEOUT_DEFAULT_S << " seconds instead." << termcolor::reset << std::endl; } } From 6ddc1efc932cabbebbc8d270d8d7f9d8946e7c1d Mon Sep 17 00:00:00 2001 From: Ian Thomas Date: Mon, 11 May 2026 12:26:10 +0100 Subject: [PATCH 3/4] Add timeout tests --- test/test_clone.py | 61 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/test/test_clone.py b/test/test_clone.py index 8a3211b..35693ae 100644 --- a/test/test_clone.py +++ b/test/test_clone.py @@ -1,5 +1,6 @@ import pytest import subprocess +from .conftest import GIT2CPP_TEST_WASM xtl_url = "https://github.com/xtensor-stack/xtl.git" @@ -144,3 +145,63 @@ def test_clone_gitlab(git2cpp_path, tmp_path, run_in_tmp_path, protocol): assert p_status.returncode == 0 assert "On branch main" in p_status.stdout assert "Your branch is up to date with 'origin/main'" in p_status.stdout + + +@pytest.mark.skipif(not GIT2CPP_TEST_WASM, reason="Only test in WebAssembly") +def test_clone_timeout(git2cpp_path, tmp_path, run_in_tmp_path): + # Set very short timeout. + subprocess.run(["export", "GIT_HTTP_TIMEOUT=0.001"], check=True) + + # Check timeout is set. + check_env_cmd = ["env"] + p_check_env = subprocess.run(check_env_cmd, capture_output=True, cwd=tmp_path, text=True) + assert "GIT_HTTP_TIMEOUT=0.001" in p_check_env.stdout + + # Clone fails with timeout. + clone_cmd = [git2cpp_path, "clone", xtl_url] + p_clone = subprocess.run(clone_cmd, capture_output=True, cwd=tmp_path, text=True) + assert p_clone.returncode != 0 + assert "network request timed out connecting to" in p_clone.stderr + assert ( + "set a longer timeout in seconds using the environment variable GIT_HTTP_TIMEOUT" + in p_clone.stderr + ) + + # Set more reasonable timeout. + subprocess.run(["export", "GIT_HTTP_TIMEOUT=10"], check=True) + + # Check timeout is set. + p_check_env = subprocess.run(check_env_cmd, capture_output=True, cwd=tmp_path, text=True) + assert "GIT_HTTP_TIMEOUT=10" in p_check_env.stdout + + # Clone succeeds. + clone_cmd = [git2cpp_path, "clone", xtl_url] + p_clone = subprocess.run(clone_cmd, capture_output=True, cwd=tmp_path, text=True) + assert p_clone.returncode == 0 + + assert (tmp_path / "xtl").exists() + assert (tmp_path / "xtl/include").exists() + + +@pytest.mark.skipif(not GIT2CPP_TEST_WASM, reason="Only test in WebAssembly") +def test_clone_negative_timeout_ignored(git2cpp_path, tmp_path, run_in_tmp_path): + # Set negative timeout. + subprocess.run(["export", "GIT_HTTP_TIMEOUT=-1"], check=True) + + # Check timeout is set. + check_env_cmd = ["env"] + p_check_env = subprocess.run(check_env_cmd, capture_output=True, cwd=tmp_path, text=True) + assert "GIT_HTTP_TIMEOUT=-1" in p_check_env.stdout + + # Clone succeeds, ignoring invalid timeout. + clone_cmd = [git2cpp_path, "clone", xtl_url] + p_clone = subprocess.run(clone_cmd, capture_output=True, cwd=tmp_path, text=True) + assert p_clone.returncode == 0 + + assert (tmp_path / "xtl").exists() + assert (tmp_path / "xtl/include").exists() + + assert ( + "environment variable GIT_HTTP_TIMEOUT must be a positive number of seconds" + in p_clone.stdout + ) From 79c20ab8f3ef8b6923caef474bca4130ecef8156 Mon Sep 17 00:00:00 2001 From: Ian Thomas Date: Mon, 11 May 2026 13:25:22 +0100 Subject: [PATCH 4/4] Add to online docs --- docs/conf.py | 1 + docs/index.md | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 8ad33a3..9b2109b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -19,3 +19,4 @@ "show_navbar_depth": 2, } html_title = "git2cpp documentation" +myst_enable_extensions = ["deflist"] diff --git a/docs/index.md b/docs/index.md index 462e7d4..81f5be3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -15,3 +15,14 @@ please create an issue in the [git2cpp github repository](https://github.com/Qua :hidden: created/git2cpp ``` + +## Environment variables + +`GIT_HTTP_TIMEOUT` +: In the WebAssembly build, all http(s) requests are limited by a timeout which has a default of 10 + seconds. To use a different timeout set the `GIT_HTTP_TIMEOUT` environment variable. For example, + to set a timeout of 20 seconds use: + + ```bash + export GIT_HTTP_TIMEOUT=20 + ```