[BUG] CoercedPredictRequest does not validate quantiles, so POST /predict/bytes accepts invalid values

Open Beginner friendly
#142 1 comment 0 reactions 0 assignees View on GitHub

@wsj040303 is already working on this.

Since Nov 7, 2026.

  • #143 by @wsj040303 — open

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
78/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
python

Research direction

Start by reading CoercedPredictRequest and the _check_quantiles validator in the request types, then compare how PredictRequest applies it. Trace decode_request to confirm the /predict/bytes path constructs the coerced request directly. Add coverage for empty, out-of-range, and duplicate quantiles on both request types; done means both routes reject them before estimator work.

Written by the indexing model from the issue text.

Description

Describe the bug

CoercedPredictRequest does not validate quantiles, even though #137 ("validate quantiles in PredictRequest and CoercedPredictRequest", commit 74c8278) was meant to cover it and the model's own docstring advertises it. Only PredictRequest got the _check_quantiles validator.

Because POST /predict/bytes builds CoercedPredictRequest directly from the multipart metadata field via decode_request, it never constructs a PredictRequest — so invalid quantiles are accepted on that route and carried all the way into the estimator.

Two user-visible consequences:

  1. quantiles: [] returns HTTP 200 with no quantile table, silently. SktimeExecutor.predict guards with if quantiles:, so an empty list is falsy and the quantile forecast is skipped without a word. The same body on POST /predict is a 422.
  2. Out-of-range and duplicate quantiles are rejected only after a full fit + point predict. The request fails at predict_quantiles with HTTP 400, after the server has already paid for an inference pass. This is the same wasted-work problem #134 fixed for unsupported-quantile capability, but quantile values still slip past on the bytes path.

Identical requests therefore behave differently depending on the route — 422 before any work on POST /predict, 400 after a full inference pass on POST /predict/bytes (the route the Python Client uses).

Reproduce the bug

Unit level — CoercedPredictRequest accepts every value _check_quantiles is supposed to reject:

import narwhals as nw

from tserve.types import CoercedPredictRequest
from tserve.types.converters import _to_bytes, decode_request

past = nw.from_dict(
    {
        "timestamp": ["2024-01-01", "2024-01-02", "2024-01-03", "2024-01-04"],
        "sales": [120.0, 135.0, 128.0, 142.0],
    },
    backend="pyarrow",
)

for bad in ([], [0.0], [1.0], [1.5], [-0.5], [0.5, 0.5]):
    req = CoercedPredictRequest(
        past=past, time="timestamp", target=["sales"], fh=2, quantiles=bad
    )
    print("accepted", req.quantiles)

# the POST /predict/bytes path, via decode_request
req = decode_request(
    {"time": "timestamp", "target": ["sales"], "fh": 2,
     "model": "naive", "quantiles": [1.5, 1.5]},
    {"past": _to_bytes(past)},
)
print("decode_request accepted", req.quantiles)
accepted []
accepted [0.0]
accepted [1.0]
accepted [1.5]
accepted [-0.5]
accepted [0.5, 0.5]
decode_request accepted [1.5, 1.5]

Route level — the same five bodies against both predict routes:

quantiles POST /predict POST /predict/bytes
[] 422 200, quantiles: null
[0.5, 0.5] 422 400 after fit + predict
[1.5] 422 400 after fit + predict
[0.0] 422 400 after fit + predict
[-0.5] 422 400 after fit + predict

The 400s carry the estimator's message (values in alpha must lie in the open interval (0, 1), but found value: 1.5) wrapped in the "returned its point forecast but failed on the requested quantiles" text — i.e. the point forecast really was computed first.

Expected behavior

CoercedPredictRequest runs _check_quantiles, like PredictRequest does, so both predict routes reject invalid quantiles before any model work and quantiles: [] is an error rather than a silent no-op.

Versions

main at 74c8278, Python 3.12, macOS.

Dominant language
Python
Stars
21
Forks
5
Avg merge
3d 10h
Merged PRs (30d)
2

Getting set up

  • Ships a Dockerfile or Docker Compose file
  • Has a pull request template
  • No contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from sktime/tserve

All issues in sktime/tserve

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.