]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
pytest: check 429 handling
authorStefan Eissing <stefan@eissing.org>
Fri, 17 Apr 2026 13:21:12 +0000 (15:21 +0200)
committerDaniel Stenberg <daniel@haxx.se>
Fri, 17 Apr 2026 20:17:50 +0000 (22:17 +0200)
Add a "limit" handler to httpd that responds 429 after 5 requests and
then 429s all requests for 2 seconds. After which another 4 requests are
served before 429 strikes again.

Closes #21357

tests/http/test_05_errors.py
tests/http/testenv/httpd.py
tests/http/testenv/mod_curltest/mod_curltest.c

index 4e5f55241232c0392a1df6f61393643b8b26ff96..374cbedff3ac99ca3b6c6f33652d23959bace026 100644 (file)
@@ -217,3 +217,15 @@ class TestErrors:
         # - CURLE_RECV_ERROR (56) - some TLS backends fail with that
         assert r.exit_code in [35, 56], \
             f'unexpected error {r.exit_code}\n{r.dump_logs()}'
+
+    # Get a resource many times with limited requests
+    @pytest.mark.skipif(condition=not Env.have_h2_curl(), reason='curl without HTTP/2')
+    def test_05_10_limits(self, env: Env, httpd):
+        proto = 'h2'
+        count = 6
+        curl = CurlClient(env=env)
+        url = f'https://{env.authority_for(env.domain1, proto)}/curltest/limit?id=[0-{count-1}]'
+        r = curl.http_download(urls=[url], alpn_proto=proto, extra_args=[
+            '--parallel', '--retry', '5'
+        ])
+        r.check_stats(count=count, http_status=200, exitcode=0)
index 06fbf25f06232b0961dca7a721478049124b0f9c..64ca796d13b521d86ec732a17dce8fb21d96376e 100644 (file)
@@ -535,6 +535,9 @@ class Httpd:
                 '    <Location /curltest/echo>',
                 '      SetHandler curltest-echo',
                 '    </Location>',
+                '    <Location /curltest/limit>',
+                '      SetHandler curltest-limit',
+                '    </Location>',
                 '    <Location /curltest/put>',
                 '      SetHandler curltest-put',
                 '    </Location>',
index bc1c75e5d9819ddbd20f346971622e9fb3b2aa19..5e0f6400fb57b06b68956dd82412f64d0eb3c8d8 100644 (file)
@@ -824,11 +824,136 @@ cleanup:
   return DECLINED;
 }
 
+struct curltest_limit_rec {
+  int rcount;
+  int rlimit;
+  apr_time_t end;
+  apr_time_t duration_sec;
+  struct apr_thread_mutex_t *lock;
+};
+
+static struct curltest_limit_rec limitrec = {
+  0, 5, 0, 2
+};
+
+static int curltest_limit_handler(request_rec *r)
+{
+  conn_rec *c = r->connection;
+  apr_bucket_brigade *bb;
+  apr_bucket *b;
+  apr_status_t rv;
+  const char *request_id = NULL;
+  int i, denied;
+  apr_time_t now;
+
+  if(strcmp(r->handler, "curltest-limit")) {
+    return DECLINED;
+  }
+  if(r->method_number != M_GET) {
+    return DECLINED;
+  }
+
+  if(r->args) {
+    apr_array_header_t *args = apr_cstr_split(r->args, "&", 1, r->pool);
+    for(i = 0; i < args->nelts; ++i) {
+      char *s, *val, *arg = APR_ARRAY_IDX(args, i, char *);
+      s = strchr(arg, '=');
+      if(s) {
+        *s = '\0';
+        val = s + 1;
+        if(!strcmp("id", arg)) {
+          /* just an id for repeated requests with curl's URL globbing */
+          request_id = val;
+          continue;
+        }
+      }
+      ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "query parameter not "
+                    "understood: '%s' in %s",
+                    arg, r->args);
+      ap_die(HTTP_BAD_REQUEST, r);
+      return OK;
+    }
+  }
+
+  ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "limit: processing");
+
+  now = apr_time_now();
+  apr_thread_mutex_lock(limitrec.lock);
+  if(limitrec.end && (now > limitrec.end)) {
+    /* reset limit */
+    ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, "limit: reset");
+    limitrec.rcount = 0;
+    limitrec.end = 0;
+  }
+  limitrec.rcount += 1;
+  denied = (limitrec.rcount > limitrec.rlimit);
+  if(denied) {
+    /* extend limit duration */
+    limitrec.end = now + apr_time_from_sec(limitrec.duration_sec);
+    ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, "limit: denied, %d request %s",
+                  limitrec.rcount, request_id);
+  }
+  else {
+    ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, "limit: ok, %d request %s",
+                  limitrec.rcount, request_id);
+  }
+  apr_thread_mutex_unlock(limitrec.lock);
+
+  r->status = denied ? 429 : 200;
+  r->clength = -1;
+  r->chunked = 1;
+  apr_table_unset(r->headers_out, "Content-Length");
+  /* Discourage content-encodings */
+  apr_table_unset(r->headers_out, "Content-Encoding");
+  if(request_id)
+    apr_table_setn(r->headers_out, "request-id", request_id);
+  apr_table_setn(r->subprocess_env, "no-brotli", "1");
+  apr_table_setn(r->subprocess_env, "no-gzip", "1");
+
+  if(denied) {
+    char *v = apr_psprintf(r->pool, "%d", limitrec.duration_sec);
+    apr_table_set(r->headers_out, "Retry-After", v);
+  }
+
+  ap_set_content_type(r, "text/plain");
+
+  bb = apr_brigade_create(r->pool, c->bucket_alloc);
+
+  apr_brigade_puts(bb, NULL, NULL, "The resource served with limits.\n");
+
+  /* flush response */
+  b = apr_bucket_flush_create(c->bucket_alloc);
+  APR_BRIGADE_INSERT_TAIL(bb, b);
+  rv = ap_pass_brigade(r->output_filters, bb);
+  if(APR_SUCCESS != rv)
+    goto cleanup;
+
+  /* we are done */
+  b = apr_bucket_eos_create(c->bucket_alloc);
+  APR_BRIGADE_INSERT_TAIL(bb, b);
+  rv = ap_pass_brigade(r->output_filters, bb);
+
+cleanup:
+  if(rv == APR_SUCCESS ||
+     r->status != HTTP_OK ||
+     c->aborted) {
+    ap_log_rerror(APLOG_MARK, APLOG_TRACE1, rv, r, "limit: done");
+    return OK;
+  }
+  else {
+    /* no way to know what type of error occurred */
+    ap_log_rerror(APLOG_MARK, APLOG_TRACE1, rv, r, "limit failed");
+    return AP_FILTER_ERROR;
+  }
+  return DECLINED;
+}
+
 static int curltest_post_config(apr_pool_t *p, apr_pool_t *plog,
                                 apr_pool_t *ptemp, server_rec *s)
 {
   void *data = NULL;
   const char *key = "mod_curltest_init_counter";
+  apr_status_t rv;
 
   (void)p;
   (void)plog;
@@ -842,9 +967,7 @@ static int curltest_post_config(apr_pool_t *p, apr_pool_t *plog,
     return APR_SUCCESS;
   }
 
-  /* mess with the overall server here */
-
-  return APR_SUCCESS;
+  return apr_thread_mutex_create(&limitrec.lock, APR_THREAD_MUTEX_DEFAULT, p);
 }
 
 static void curltest_hooks(apr_pool_t *pool)
@@ -861,6 +984,7 @@ static void curltest_hooks(apr_pool_t *pool)
   ap_hook_handler(curltest_tweak_handler, NULL, NULL, APR_HOOK_MIDDLE);
   ap_hook_handler(curltest_1_1_required, NULL, NULL, APR_HOOK_MIDDLE);
   ap_hook_handler(curltest_sslinfo_handler, NULL, NULL, APR_HOOK_MIDDLE);
+  ap_hook_handler(curltest_limit_handler, NULL, NULL, APR_HOOK_MIDDLE);
 }
 
 AP_DECLARE_MODULE(curltest) =