Skip to content

fix(triggers): support POST with query params for autoscan compatibility#522

Open
jkristoph wants to merge 1 commit into
dan-online:mainfrom
jkristoph:fix/autoscan-post-query-params
Open

fix(triggers): support POST with query params for autoscan compatibility#522
jkristoph wants to merge 1 commit into
dan-online:mainfrom
jkristoph:fix/autoscan-post-query-params

Conversation

@jkristoph
Copy link
Copy Markdown

Summary

  • Fixes autoscan-to-autoscan forwarding compatibility
  • The original autoscan sends scan requests between instances as POST /triggers/manual?dir=/path with an empty body (see targets/autoscan/api.go)
  • The current POST handler requires a JSON body and rejects Manual/Autoscan trigger types, making it incompatible
  • This was previously documented in feat: Autoscan Legacy Trigger #307, which included an nginx workaround

Changes

  • Modified trigger_post to accept raw Bytes instead of Json<Value>, allowing empty bodies
  • When the body is empty or not valid JSON, the handler falls back to query parameter parsing (same logic as the GET handler)
  • Extracted shared query-param handling into trigger_get_inner() to avoid code duplication
  • Existing JSON-body triggers (Sonarr, Radarr, etc.) continue to work unchanged

How autoscan sends requests

From targets/autoscan/api.go in the autoscan repo:

req, err := http.NewRequestWithContext(ctx, http.MethodPost, triggerURL, http.NoBody)
q := url.Values{}
q.Add("dir", folder)
req.URL.RawQuery = q.Encode()

Test plan

  • POST /triggers/<autoscan-type>?dir=/path with no body → should succeed (was failing)
  • POST /triggers/<manual-type>?path=/file with no body → should succeed (was failing)
  • GET /triggers/<autoscan-type>?dir=/path → should still work (unchanged)
  • GET /triggers/<manual-type>?path=/file → should still work (unchanged)
  • POST /triggers/sonarr with JSON body → should still work (unchanged)
  • HEAD /triggers/manual health check → should still work (unchanged)

🤖 Generated with Claude Code

The original autoscan (github.com/cloudbox/autoscan) sends scan requests
between instances as POST /triggers/manual?dir=/path with an empty body.
The current POST handler requires a JSON body and rejects Manual/Autoscan
trigger types, making it incompatible with autoscan-to-autoscan forwarding.

This change makes the POST handler fall back to query parameter parsing
(the same logic used by the GET handler) when the request body is empty
or not valid JSON. This enables seamless interop with legacy autoscan
instances without breaking existing JSON-body-based triggers (Sonarr,
Radarr, etc.).

Closes dan-online#307

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment on lines +55 to +72
let parsed_body: Option<serde_json::Value> = if body.is_empty() {
None
} else {
serde_json::from_slice(&body).ok()
};

let has_body = parsed_body
.as_ref()
.map(|b| !b.is_null())
.unwrap_or(false);

if !has_body {
if let Ok(query) =
Query::<TriggerQueryParams>::from_query(req.query_string())
{
return trigger_get_inner(&trigger, query.into_inner(), &manager, trigger_settings)
.await;
}
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Nice direction here. I think the cleanest fix is to parse the query string based on trigger_settings instead of going through the untagged enum first. That lets manual normalize the legacy dir shape explicitly, and it keeps invalid non-empty JSON failing fast instead of silently falling back.

Something like:

Suggested change
let parsed_body: Option<serde_json::Value> = if body.is_empty() {
None
} else {
serde_json::from_slice(&body).ok()
};
let has_body = parsed_body
.as_ref()
.map(|b| !b.is_null())
.unwrap_or(false);
if !has_body {
if let Ok(query) =
Query::<TriggerQueryParams>::from_query(req.query_string())
{
return trigger_get_inner(&trigger, query.into_inner(), &manager, trigger_settings)
.await;
}
let query_fallback = if body.is_empty() {
match trigger_settings {
Trigger::Manual(_) | Trigger::Bazarr(_) => Query::<ManualQueryParams>::from_query(req.query_string())
.map(|q| TriggerQueryParams::Manual(q.into_inner()))
.or_else(|_| {
Query::<AutoscanQueryParams>::from_query(req.query_string()).map(|q| {
TriggerQueryParams::Manual(ManualQueryParams {
path: q.into_inner().dir,
hash: None,
})
})
})
.ok(),
Trigger::Autoscan(_) => Query::<AutoscanQueryParams>::from_query(req.query_string())
.map(|q| TriggerQueryParams::Autoscan(q.into_inner()))
.ok(),
_ => None,
}
} else {
None
};
if let Some(query) = query_fallback {
return trigger_get_inner(&trigger, query, &manager, trigger_settings).await;
}
let parsed_body: Option<serde_json::Value> = if body.is_empty() {
None
} else {
Some(
serde_json::from_slice(&body)
.map_err(|_| actix_web::error::ErrorBadRequest("Invalid JSON body"))?,
)
};

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Not implemented feels a little odd here. I think 400 Invalid request would be a safer response for unsupported combinations.

Something like:

Suggested change
_ => Ok(HttpResponse::BadRequest().body("Invalid request")),

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.

2 participants