Skip to content

feat(api): add async query parameter support - #1609

Merged
nicholas-fedor merged 11 commits into
mainfrom
feat/1575-add-HTTP-API-HEAD-support
May 10, 2026
Merged

feat(api): add async query parameter support#1609
nicholas-fedor merged 11 commits into
mainfrom
feat/1575-add-HTTP-API-HEAD-support

Conversation

@nicholas-fedor

@nicholas-fedor nicholas-fedor commented May 10, 2026

Copy link
Copy Markdown
Owner

This PR adds an ?async=true query parameter to the /v1/update endpoint, allowing clients to trigger updates without waiting for completion.

Problem

As noted by issue #1575, Watchtower's HTTP API currently maintains a long-lived connection when a POST request is sent to trigger updates. This is problematic for execution environments, such as GitHub Actions, where connection time is billed and/or it's not necessary to wait for the update cycle to finish and receive a final response.

Solution

Issue #1575 proposed using the HEAD method for this; however, that would have been noncompliant with RFC 9110 Section 9.3.2 (HEAD) and Section 9.2.1 (Safe Methods).

Instead of using a noncompliant HEAD endpoint, an ?async=true query parameter was added to the existing POST endpoint. When present, the handler spawns the update in a background goroutine and returns 202 Accepted immediately.

  • POST /v1/update — synchronous, returns 200 with JSON results (unchanged)
  • POST /v1/update?async=true — asynchronous, returns 202 Accepted, runs update in background

Changes

  • pkg/api/update/update.go — Added ?async=true branching in Handle; extracted handler logic into focused helper methods
  • internal/api/api.go — Removed HEAD method registration
  • pkg/api/update/update_test.go — Added async parameter tests; added tests for body size limits, context cancellation, and panic recovery
  • docs/advanced-features/http-api/index.md — Documented ?async=true parameter and asynchronous behavior
  • pkg/api/update/doc.go — Updated package description

Summary by CodeRabbit

  • New Features

    • Added asynchronous update support via ?async=true — requests return HTTP 202 and run updates in the background for non-blocking operations; targeted updates may block or return 503 if canceled while waiting.
  • Documentation

    • Updated HTTP API docs with async behavior, examples, updated status codes (202/503/429 etc.), and merged 429 example placement.
  • Tests

    • Expanded coverage for async flows, request-body limits, lock/cancellation, panic recovery, and response-write error handling.

Review Change Stack

Implement support for the HTTP HEAD method on the update API.
HEAD requests trigger the update process asynchronously and return
HTTP 202 Accepted immediately, allowing clients to initiate
updates without waiting for completion.

- Register HEAD method in the API router
- Update handler to process HEAD requests asynchronously
- Ensure lock management and panic recovery in async goroutines
- Add unit tests for HEAD request behavior (202 Accepted, 429 Too Many
  Requests, and blocking behavior)
Refactor the monolithic Handle function into 11 focused, well-documented helper
methods to improve maintainability, testability, and code clarity. Preserve existing
behavior — HEAD async updates (202 Accepted) and POST synchronous updates (200 OK).

Changes:
- Extract readRequestBody, extractImages, acquireLock, send429Response
- Extract handleHead, handlePost, executeUpdateAsync, executeUpdate, writeSuccessResponse, releaseLock
- Add consistent package-level and function doc comments with Parameters/Returns
- Add 6 black-box error-path tests: oversized body rejection, cancellation handling,
  panic recovery in async goroutines, write error handling for both 429 and 200 paths,
  and early return validation
- Increase test coverage from ~87% to 95%
- Fix noctx lint violations by using NewRequestWithContext throughout tests
- introduce lockResult struct for acquireLock return values
- consolidate Token, Acquired, and RequestErr into single return type
- update Handle method to use new lockResult struct fields
Fix the violation of RFC 9110 safe method semantics and use an async query parameter instead.

- remove HEAD method registration from update handler endpoint
- add async=true query parameter support for asynchronous updates
- rename handleHead to handleAsync with updated logging
- update tests to use POST with async parameter instead of HEAD method
…eter

- add async query parameter documentation to update endpoint table
- document HTTP 202 Accepted response for async updates
- include curl examples for fire-and-forget update scenarios
- update package documentation to reflect async execution support
@nicholas-fedor nicholas-fedor linked an issue May 10, 2026 that may be closed by this pull request
2 tasks
@coderabbitai

coderabbitai Bot commented May 10, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6719e03a-e515-4289-b97e-100c9aed873a

📥 Commits

Reviewing files that changed from the base of the PR and between 8a81d68 and 0344e04.

📒 Files selected for processing (1)
  • docs/advanced-features/http-api/index.md

Walkthrough

Adds async execution to POST /v1/update via ?async=true. Handler refactored into helpers for body reading, image extraction, lock acquisition, async vs sync execution, response encoding, and lock release. Docs and tests updated to cover async behavior, status codes, and edge cases.

Changes

Async Update Handler Implementation

Layer / File(s) Summary
API Documentation & Intent
docs/advanced-features/http-api/index.md, pkg/api/update/doc.go
HTTP API docs and package doc updated to document async parameter, add HTTP 202/503 statuses, and add an "Asynchronous Updates" section with examples.
Data Types & Constants
pkg/api/update/update.go
Introduces lockResult struct and request-related constants (max body bytes, retry wait).
Main Handler Control Flow & Initialization
pkg/api/update/update.go
(*Handler).Handle refactored to use helpers: read/discard body, extract image params, acquire lock, then branch to async or sync paths; init logging adjusted for provided lock channel.
Request & Parameter Processing
pkg/api/update/update.go
Adds readRequestBody (uses http.MaxBytesReader, returns 413/500 on errors) and extractImages (aggregates comma-separated image params).
Lock Management & Release
pkg/api/update/update.go
Adds acquireLock implementing non-blocking full-update (429) vs blocking targeted-update (wait or 503 on context cancel); adds releaseLock.
Async & Sync Execution Handlers
pkg/api/update/update.go
Adds handleAsync (spawn goroutine, return 202), handlePost (sync execution, defer release), executeUpdateAsync (panic recovery, timing), and executeUpdate (sync timing).
Response Encoding & Error Handling
pkg/api/update/update.go
Adds send429Response (429 JSON with Retry-After and timestamp) and updates writeSuccessResponse to JSON-encode metrics and return 500 on encoding failure.
Comprehensive Test Coverage
pkg/api/update/update_test.go
Adds multiple tests: async full/targeted behavior (202/429/blocking), oversized/unreadable body (413/500), targeted lock cancel (503), async panic recovery and lock release, write error handling, and invalid body early return.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • nicholas-fedor/watchtower#1506: Both PRs modify the update HTTP handler and its tests—touching pkg/api/update/update.go and response-summary behavior.
  • nicholas-fedor/watchtower#1304: Both PRs modify pkg/api/update/update.go's update handler and locking semantics for full vs targeted updates (including the 429-on-locked full-update behavior and related tests).
  • nicholas-fedor/watchtower#1342: Updates HTTP API documentation for the update endpoint status codes and concurrency behavior.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main feature being added: async query parameter support for the HTTP API.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codacy-production

codacy-production Bot commented May 10, 2026

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 1 medium

Alerts:
⚠ 1 issue (≤ 0 issues of at least minor severity)

Results:
1 new issue

Category Results
ErrorProne 1 medium

View in Codacy

🟢 Metrics 19 duplication

Metric Results
Duplication 19

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@codecov

codecov Bot commented May 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.05263% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
pkg/api/update/update.go 96.05% 2 Missing and 1 partial ⚠️

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1609      +/-   ##
==========================================
+ Coverage   75.04%   75.24%   +0.20%     
==========================================
  Files          58       58              
  Lines        9795     9830      +35     
==========================================
+ Hits         7351     7397      +46     
+ Misses       2183     2174       -9     
+ Partials      261      259       -2     
Files with missing lines Coverage Δ
pkg/api/update/update.go 95.07% <96.05%> (+10.02%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/api/update/update.go (1)

156-160: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Drop blank image entries while parsing the query string.

?image= and values with trailing commas currently produce []string{""}, which sends the request down the targeted-update path and changes lock behavior from immediate 429 to blocking. Filtering empty segments here avoids that edge-case semantic shift.

Suggested fix
 	if found {
 		for _, image := range imageQueries {
-			images = append(images, strings.Split(image, ",")...)
+			for _, part := range strings.Split(image, ",") {
+				part = strings.TrimSpace(part)
+				if part == "" {
+					continue
+				}
+				images = append(images, part)
+			}
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/api/update/update.go` around lines 156 - 160, When parsing
r.URL.Query()["image"] into imageQueries and appending to images, filter out
empty segments so that inputs like "?image=" or trailing commas don't produce ""
entries; update the loop that iterates over imageQueries (and the
strings.Split(image, ",") result) to skip any empty strings before appending to
images (e.g., filter split segments or use a split-and-filter helper) so images
contains only non-empty values and avoids triggering the targeted-update path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/advanced-features/http-api/index.md`:
- Around line 138-145: Add a 503 entry to the "Status Codes for Async Requests"
table (under the "Status Codes for Async Requests" heading) indicating "503
Service Unavailable — Client cancelled while waiting on update lock" and also
add the same 503 row to the main status-code table above so both tables mirror
each other; locate and update the tables in
docs/advanced-features/http-api/index.md where the current 202/401/429/500 rows
are defined.
- Around line 132-133: The curl example in
docs/advanced-features/http-api/index.md uses an unquoted URL with an ampersand
so most shells will treat & as a background operator and drop the async=true
parameter; update the POST example (the curl command shown) to quote the full
URL (e.g., "http://localhost:8080/v1/update?image=foo/bar:latest&async=true") or
escape the ampersand so the async=true query parameter is preserved when run in
a shell.
- Around line 12-15: The table in docs/advanced-features/http-api/index.md uses
intra-page anchors with underscores (e.g. [`image`](`#image_parameter_usage`),
[`async`](`#asynchronous_updates`)) which don't match the actual heading anchors;
update those fragment links to use hyphenated anchors that match the headings
(for example change `#image_parameter_usage` -> `#image-parameter-usage` and
`#asynchronous_updates` -> `#asynchronous-updates`) and verify the
[Update](`#http_api_update`) link matches the "http api update" heading format
(e.g., use `#http-api-update` or the exact generated slug for the "Update"
heading) so all table links resolve correctly.

---

Outside diff comments:
In `@pkg/api/update/update.go`:
- Around line 156-160: When parsing r.URL.Query()["image"] into imageQueries and
appending to images, filter out empty segments so that inputs like "?image=" or
trailing commas don't produce "" entries; update the loop that iterates over
imageQueries (and the strings.Split(image, ",") result) to skip any empty
strings before appending to images (e.g., filter split segments or use a
split-and-filter helper) so images contains only non-empty values and avoids
triggering the targeted-update path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d162e685-ab09-402d-b734-66b4c3158614

📥 Commits

Reviewing files that changed from the base of the PR and between 37d2d3e and 2e7eb64.

📒 Files selected for processing (4)
  • docs/advanced-features/http-api/index.md
  • pkg/api/update/doc.go
  • pkg/api/update/update.go
  • pkg/api/update/update_test.go

Comment thread docs/advanced-features/http-api/index.md
Comment thread docs/advanced-features/http-api/index.md Outdated
Comment thread docs/advanced-features/http-api/index.md

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
docs/advanced-features/http-api/index.md (1)

12-15: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix the broken intra-page anchor links in the endpoints table.

The anchor fragments in line 14 use underscores (#http_api_update, #image_parameter_usage, #asynchronous_updates), but Markdown generates hyphenated heading IDs. These links won't resolve to the actual headings.

🔗 Proposed fix
-|     [Update](`#http_api_update`)     |   `POST`   | `/v1/update`  | [`image`](`#image_parameter_usage`), [`async`](`#asynchronous_updates`) | Triggers container updates and returns JSON results of the operation |
+|     [Update](`#http-api-update`)     |   `POST`   | `/v1/update`  | [`image`](`#image-parameter-usage`), [`async`](`#asynchronous-updates`) | Triggers container updates and returns JSON results of the operation |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/advanced-features/http-api/index.md` around lines 12 - 15, The table's
intra-page anchor links use underscores and thus don't match Markdown's
hyphenated heading IDs; update the anchor fragments in the endpoints table so
[Update](`#http_api_update`) → [Update](`#http-api-update`),
[`image`](`#image_parameter_usage`) → [`image`](`#image-parameter-usage`), and
[`async`](`#asynchronous_updates`) → [`async`](`#asynchronous-updates`) so they
resolve to the actual headings (update the exact strings shown in the diff).
🧹 Nitpick comments (1)
docs/advanced-features/http-api/index.md (1)

121-121: 💤 Low value

Consider quoting the URL for consistency.

While this URL is safe without quotes (no & character), quoting it would match the style used in line 134 and provide a consistent pattern across all curl examples.

📝 Suggested change
-curl -X POST -H "Authorization: Bearer mytoken" localhost:8080/v1/update?async=true
+curl -X POST -H "Authorization: Bearer mytoken" "localhost:8080/v1/update?async=true"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/advanced-features/http-api/index.md` at line 121, Update the curl
example that currently reads: curl -X POST -H "Authorization: Bearer mytoken"
localhost:8080/v1/update?async=true so the request URL is quoted (e.g., "...
\"localhost:8080/v1/update?async=true\"") to match the quoting style used in
other examples; locate the curl example text in the markdown and wrap the whole
URL argument in quotes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@docs/advanced-features/http-api/index.md`:
- Around line 12-15: The table's intra-page anchor links use underscores and
thus don't match Markdown's hyphenated heading IDs; update the anchor fragments
in the endpoints table so [Update](`#http_api_update`) →
[Update](`#http-api-update`), [`image`](`#image_parameter_usage`) →
[`image`](`#image-parameter-usage`), and [`async`](`#asynchronous_updates`) →
[`async`](`#asynchronous-updates`) so they resolve to the actual headings (update
the exact strings shown in the diff).

---

Nitpick comments:
In `@docs/advanced-features/http-api/index.md`:
- Line 121: Update the curl example that currently reads: curl -X POST -H
"Authorization: Bearer mytoken" localhost:8080/v1/update?async=true so the
request URL is quoted (e.g., "... \"localhost:8080/v1/update?async=true\"") to
match the quoting style used in other examples; locate the curl example text in
the markdown and wrap the whole URL argument in quotes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 300ebc92-9c29-4662-b187-8b513b1b41c9

📥 Commits

Reviewing files that changed from the base of the PR and between 2e7eb64 and 8a81d68.

📒 Files selected for processing (1)
  • docs/advanced-features/http-api/index.md

@nicholas-fedor
nicholas-fedor merged commit 8a85c33 into main May 10, 2026
17 of 18 checks passed
@nicholas-fedor
nicholas-fedor deleted the feat/1575-add-HTTP-API-HEAD-support branch May 10, 2026 04:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow HEAD method to request update and not wait for result

1 participant