Skip to content

Path-level parameters are parsed then dropped: PathItem.parameters never reach request validation #61

Description

@shadowhand

PHP version

8.5

duyler/openapi version

0.7.0

OpenAPI spec version

3.0

Description

Parameters declared on a Path Item — paths./x.parameters, which the OpenAPI specification defines as applying to all operations under that path — are parsed into the schema model and then dropped before validation. They are never enforced.

PathItem does hold them:

// src/Schema/Model/PathItem.php:32
public ?Parameters $parameters = null,

But RequestValidator::validate() receives only the Operation and builds its parameter set from that alone:

// src/Validator/Request/RequestValidator.php:37
$parameters = $operation->parameters?->parameters ?? [];

/** @var list<Parameter> $parameterSchemas */
$parameterSchemas = array_filter($parameters, fn($param) => $param instanceof Parameter);

$pathParams = $this->pathParser->matchPath(
    $request->getUri()->getPath(),
    $pathTemplate,
);
$this->pathParamsValidator->validate($pathParams, $parameterSchemas);

$queryString = $request->getUri()->getQuery();
$queryParams = $this->queryParser->parse($queryString);
$this->queryParamsValidator->validate($queryParams, $parameterSchemas);

The path item's parameters never reach $parameterSchemas, so the same $parameterSchemas is passed to every location validator with the path-level entries missing. RequestValidator is given $pathTemplate but not the PathItem, so it has no way to recover them.

Consequences, all silent:

  • required: true on a path-level parameter is not enforced — omitting it passes.
  • schema constraints on path-level parameters (enum, format, minimum, additionalProperties: false, …) are not checked.
  • This applies to in: path as much as in: query. Path parameters are the usual thing to declare at the path item level, since they are shared by every operation on that path, so hoisting them there — exactly as the specification encourages — turns their validation off.

The failure mode is worse than a false rejection: nothing errors, so a spec author has no signal that a whole class of parameters is unvalidated. In our own spec, moving parameters to the path item (the DRY choice for a path with several operations) silently disabled validation for 59 parameter declarations, 45 of them in: path and 46 marked required, and every test still passed.

Per the specification, the merge rule is that path item parameters apply to all operations under the path, and an operation-level parameter with the same name and in overrides the path-level one; the two cannot otherwise duplicate.

Steps to reproduce

<?php

declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use Duyler\OpenApi\Builder\OpenApiValidatorBuilder;
use Nyholm\Psr7\ServerRequest;

$yaml = <<<'YAML'
openapi: 3.0.0
info:
  title: Path Level Parameters API
  version: 1.0.0
paths:
  /widgets/{widgetId}:
    parameters:
      - name: widgetId
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: mustHave
        in: query
        required: true
        schema:
          type: string
          enum: [alpha, beta]
    get:
      responses:
        '200':
          description: ok
YAML;

$validator = OpenApiValidatorBuilder::create()
    ->fromYamlString($yaml)
    ->enableCoercion()
    ->build();

$cases = [
    'required query param omitted     ' => '/widgets/2b3e0c4a-59f4-4c4f-9a0a-1e0d9c7f0b11',
    'query param violates enum        ' => '/widgets/2b3e0c4a-59f4-4c4f-9a0a-1e0d9c7f0b11?mustHave=gamma',
    'path param violates format: uuid ' => '/widgets/not-a-uuid?mustHave=alpha',
];

foreach ($cases as $label => $uri) {
    $validator->reset();
    parse_str((string) parse_url($uri, PHP_URL_QUERY), $query);
    $request = new ServerRequest('GET', 'http://localhost' . $uri);
    $request = $request->withQueryParams($query);

    try {
        $validator->validateRequest($request);
        printf("ACCEPTED  %s  %s\n", $label, $uri);
    } catch (Throwable $e) {
        printf("REJECTED  %s  %s\n          %s\n", $label, $uri, $e->getMessage());
    }
}

Actual result

Every violation is accepted:

ACCEPTED  required query param omitted       /widgets/2b3e0c4a-59f4-4c4f-9a0a-1e0d9c7f0b11
ACCEPTED  query param violates enum          /widgets/2b3e0c4a-59f4-4c4f-9a0a-1e0d9c7f0b11?mustHave=gamma
ACCEPTED  path param violates format: uuid   /widgets/not-a-uuid?mustHave=alpha

Expected: all three rejected — a missing required parameter, an enum violation, and a format: uuid violation respectively. Moving both parameters from paths./widgets/{widgetId}.parameters into paths./widgets/{widgetId}.get.parameters makes all three reject as they should, which isolates the path-level declaration as the trigger.

The fix is to merge PathItem.parameters into the operation's set before validation, with operation-level entries overriding path-level ones on matching name + in. Since RequestValidator currently has no access to the PathItem, the merge probably belongs where the operation is resolved from the path item, so that $operation->parameters is already complete by the time it reaches request validation.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions