Return ASCOM INVALID_VALUE for out-of-range integer parameters#14
Return ASCOM INVALID_VALUE for out-of-range integer parameters#14ivonnyssen wants to merge 6 commits intoRReverser:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates Alpaca server parameter parsing to distinguish unparseable integer inputs (HTTP 400) from parseable but out-of-range integer inputs (ASCOM INVALID_VALUE / 1025), and ensures u32 request parameters can exceed i32::MAX by widening to an intermediate signed type first.
Changes:
- Introduces a custom serde
Deserializerfor request parameters that parses integers via an intermediate type, enabling explicit range-error classification. - Adds a new
Error::ParameterOutOfRangeand maps it to ASCOMINVALID_VALUEin transaction-wrapped responses. - Adds unit tests for integer parsing/range behavior in
params.rs.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
src/server/params.rs |
Replaces serde_plain::from_str parsing with a custom deserializer and adds tests for integer parsing and range classification. |
src/server/error.rs |
Adds a new ParameterOutOfRange error variant and updates HTTP status mapping in IntoResponse. |
src/server/response.rs |
Maps ParameterOutOfRange to an ASCOM INVALID_VALUE response (HTTP 200) when returning ResponseWithTransaction<Result<T>>. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| match self.response { | ||
| Ok(response) => Ok(Ok(response)), | ||
| Err(Error::Ascom(err)) => Ok(Err(err)), | ||
| Err(Error::ParameterOutOfRange { | ||
| name, | ||
| value, | ||
| target_type, | ||
| }) => Ok(Err(ASCOMError::invalid_value(format!( | ||
| "Parameter {name:?} value {value} is out of range for {target_type}" | ||
| )))), | ||
| Err(err @ (Error::MissingParameter { .. } | Error::BadParameter { .. })) => { | ||
| Err((StatusCode::BAD_REQUEST, err.to_string())) | ||
| } |
| impl IntoResponse for Error { | ||
| fn into_response(self) -> Response { | ||
| let code = match self { | ||
| Self::UnknownDeviceNumber { .. } | Self::UnknownAction { .. } => StatusCode::NOT_FOUND, | ||
| Self::MissingParameter { .. } | Self::BadParameter { .. } => StatusCode::BAD_REQUEST, | ||
| Self::MissingParameter { .. } | ||
| | Self::BadParameter { .. } | ||
| | Self::ParameterOutOfRange { .. } => StatusCode::BAD_REQUEST, | ||
| Self::Ascom(_) => StatusCode::INTERNAL_SERVER_ERROR, | ||
| }; | ||
| (code, format!("{self:#}")).into_response() |
Analysis of Copilot review comments 3 & 4 (out-of-range parameters escaping the transaction wrapper)Updated 2026-04-21: code snippet refreshed to match the new Copilot flagged that out-of-range integer errors during How the issue arises
So sending e.g. The device parameters that ConformU actually tests (like Proposed fixCatch the out-of-range case in // mod.rs — add to imports:
use super::params::AlpacaParseError;
use crate::ASCOMError;
// mod.rs — replace line 106:
// let request_transaction = RequestTransaction::extract(&mut self.params)?;
// with:
let request_transaction = match RequestTransaction::extract(&mut self.params) {
Ok(t) => t,
Err(Error::BadParameter {
name,
err: AlpacaParseError::OutOfRange { value, target_type },
}) => {
return Ok(ResponseWithTransaction {
transaction: ResponseTransaction::new(None),
response: Err::<(), _>(ASCOMError::invalid_value(format!(
"Parameter {name:?} value {value} is out of range for {target_type}"
))),
}
.into_response());
}
Err(e) => return Err(e.into()),
};This works because I also considered a two-phase extraction approach (making Let me know if you'd like this fix included in the PR or if you'd prefer to leave it for a follow-up. — Igor (via Claude Code) |
RReverser
left a comment
There was a problem hiding this comment.
uint32 overflow: ClientID and ClientTransactionID are uint32 per spec, so values above i32::MAX (e.g. 2147483648) must parse successfully. The previous code used serde_plain::from_str which parses directly to the target type with no intermediate widening, so valid uint32 values were rejected. Using i64 as the intermediate type covers the full uint32 range.
I've been re-reading this several times and I don't get it. How is parsing directly to the target type (u32) a bad thing, but using an unrelated i64 type a good thing?
| /// This allows distinguishing parse errors (`BadParameter`) from range errors | ||
| /// (`INVALID_VALUE`), and supports both i32 device parameters and uint32 | ||
| /// transaction/identity parameters (`ClientID`, `ClientTransactionID`). | ||
| struct AlpacaDeserializer { |
There was a problem hiding this comment.
If we have custom deserializer, why do we still keep serde_plain around?
There was a problem hiding this comment.
gone with the latest push
Alpaca is a proper noun, not an acronym. Co-Authored-By: Claude Opus 4.7 <[email protected]>
Make AlpacaParseError pub(crate) and carry it directly on Error::BadParameter. The ResponseWithTransaction<Result<T>> IntoResponse impl now destructures the inner AlpacaParseError::OutOfRange to produce ASCOM INVALID_VALUE, while plain BadFormat errors continue to return HTTP 400. Co-Authored-By: Claude Opus 4.7 <[email protected]>
The custom AlpacaDeserializer now handles all Alpaca parameter parsing, so the serde_plain helper crate is no longer needed. Co-Authored-By: Claude Opus 4.7 <[email protected]>
|
@RReverser you're right — the PR description framing was wrong and I've rewritten it. The i64 intermediate isn't about widening u32; it's the narrowest type that covers the full range of every Alpaca numeric target (i8/i16/i32/u8/u16/u32/usize). That lets a single generic I've also addressed your three inline comments in the latest push:
|
Summary
Replace
serde_plain::from_strwith a custom deserializer so Alpaca integer parameters that parse successfully but don't fit the target type return ASCOMINVALID_VALUE(error 1025) instead of HTTP 400BadRequest.Closes #5
Motivation
The ASCOM Alpaca spec and ConformU conformance tests require
INVALID_VALUEfor parseable-but-out-of-range values — e.g. sendingId=999to aSwitchwith only 4 ports should return HTTP 200 withErrorNumber: 1025, not HTTP 400.Previously,
OpaqueParams::maybe_extractusedserde_plain::from_str::<T>, which surfaces a singleParseIntErrorfor every failure mode: malformed input, negative values for unsigned targets, and values too large for the target type. Everything collapsed intoError::BadParameter→ HTTP 400, so ConformU's out-of-range checks were reported as protocol errors instead of device-level errors.How it works
Integer parsing now goes through a two-step split:
str.parse::<i64>()— fails only for genuinely malformed input (AlpacaParseError::BadFormat→ HTTP 400)T::try_from(i64_value)— fails only for values that parse as integers but don't fit the target (AlpacaParseError::OutOfRange→ ASCOMINVALID_VALUE)i64is the narrowest intermediate that covers the full range of every Alpaca numeric target (i8/i16/i32/u8/u16/u32/usize — including u32 transaction IDs up to 4.3B and negative i32 device values), so a single genericparse_integer<T: TryFrom<i64>>handles all of them without per-type dispatch.The inner
AlpacaParseErroris carried onError::BadParameter, and theResponseWithTransaction<Result<T>>IntoResponseimpl destructures theOutOfRangevariant to produce the ASCOM-formatted error response (HTTP 200 + 1025).BadFormatcontinues to return HTTP 400.Changes
src/server/params.rs:AlpacaParseErrorenum (BadFormat/OutOfRange) andAlpacaDeserializerimplementingserde::DeserializerTryFromnarrowing; float/bool/char/enum/etc. paths preservedsrc/server/error.rs:Error::BadParameternow carriesAlpacaParseError(wasserde_plain::Error)src/server/response.rs:BadParameter { err: OutOfRange { .. } }maps toASCOMError::invalid_valueCargo.toml: dropserde_plaindependency (no longer needed)Test plan
cargo check --features server,all-devicespassescargo clippy --features server,all-devicescleanparams.rspass (10/10)Switch.Idreturns ASCOM 1025, not HTTP 400Id=abc) still returns HTTP 400