libopenapi-validator icon indicating copy to clipboard operation
libopenapi-validator copied to clipboard

Query Parameter Validation Miss

Open its-hammer-time opened this issue 10 months ago • 0 comments

It looks like there's a bug when it comes to query parameter validation. I haven't done a deep dive on where it failed in libopenapi-validation, but it looks like the validation works correctly with openapi-core which is why I believe there's an issue.

For this schema, I would expect a list of <=10 items is fine, but anything >10 would be invalid. The explode has been disabled so everything is under a single id field then the schema has been flagged as an array type.

package main

import (
	"fmt"
	"net/http"
	"strings"

	"github.com/pb33f/libopenapi"
	validator "github.com/pb33f/libopenapi-validator"
	"github.com/pb33f/libopenapi/datamodel"
)

const openAPISpec = `
openapi: 3.1.0
info:
  title: ID List API
  version: "1.0.0"
paths:
  /items:
    get:
      parameters:
        - name: id
          in: query
          required: true
          style: form
          explode: false
          schema:
            type: array
            items:
              type: integer
            maxItems: 10
      responses:
        '200':
          description: OK
        '400':
          description: Invalid input
`

func main() {
	// Load the OpenAPI spec from the string
	doc, err := libopenapi.NewDocumentWithConfiguration([]byte(openAPISpec), &datamodel.DocumentConfiguration{})
	if err != nil {
		panic(err)
	}

	highLevelValidator, errs := validator.NewValidator(doc)
	if errs != nil {
		panic(errs)
	}

	// Helper to build a request
	makeRequest := func(ids []int) *http.Request {
		values := make([]string, len(ids))
		for i, id := range ids {
			values[i] = fmt.Sprintf("%d", id)
		}

		// query := url.Values{}
		// query.Set("id", strings.Join(values, ","))

		req, _ := http.NewRequest(http.MethodGet, "/items?id="+strings.Join(values, ","), nil)
		return req
	}

	// Test valid case (10 items)
	validReq := makeRequest([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
	_, errs2 := highLevelValidator.ValidateHttpRequest(validReq)
	if errs2 != nil {
		panic(errs2)
	}

	// Test invalid case (12 items)
	invalidReq := makeRequest([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12})
	_, errs2 = highLevelValidator.ValidateHttpRequest(invalidReq)
	if errs2 != nil {
		panic(errs2)
	}

}

This should be a working example (just need to install the dependencies). The valid request works and doesn't raise an exception, but the invalid request raises an exception.

import yaml
from openapi_core import OpenAPI
from openapi_core.validation.request.datatypes import RequestParameters
from pyramid.request import Request
from werkzeug.test import create_environ

# OpenAPI spec defining a query parameter "id" as an array with a maxItems constraint.
openapi_spec = """
openapi: 3.1.0
info:
  title: ID List API
  version: "1.0.0"
paths:
  /items:
    get:
      parameters:
        - name: id
          in: query
          required: true
          style: form
          explode: false
          schema:
            type: array
            maxItems: 10
            items:
              type: integer
      responses:
        '200':
          description: OK
        '400':
          description: Invalid input
"""

# Load the spec and create the OpenAPI validator.
spec_dict = yaml.safe_load(openapi_spec)
spec = OpenAPI.from_dict(spec_dict)


class OpenAPIRequest:
    def __init__(self, request: Request) -> None:
        self.request: Request = request
        self.parameters = RequestParameters(
            path=self.request.matchdict or {},
            query=self.request.GET,
            header=self.request.headers,
            cookie=self.request.cookies,
        )

    @property
    def host_url(self) -> str:
        return "https://example.com"

    @property
    def path(self) -> str:
        return self.request.path

    @property
    def path_pattern(self) -> str:
        path_pattern = (
            self.request.matched_route.pattern
            if self.request.matched_route
            else self.request.path_info
        )
        return path_pattern

    @property
    def method(self) -> str:
        return self.request.method.lower()

    @property
    def body(self) -> bytes | None:
        return self.request.body

    @property
    def content_type(self) -> str:
        return self.request.content_type

    @property
    def mimetype(self) -> str:
        return self.request.content_type


def make_request(ids):
    """
    Create a fake GET /items request with a query parameter 'id'
    that is a comma-separated list of integers.
    """
    id_str = ",".join(str(i) for i in ids)
    environ = create_environ(method="GET", path="/items", query_string=f"id={id_str}")
    req = Request(environ)

    # Wrap the Werkzeug request so openapi-core can use it.
    return OpenAPIRequest(req)


# Validate a valid request (10 items)
valid_req = make_request([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
try:
    result_invalid = spec.validate_request(valid_req)
except Exception as e:
    print("Valid request failed validation with exception:", e)

# Validate an invalid request (12 items)
invalid_req = make_request([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
try:
    result_invalid = spec.validate_request(invalid_req)
except Exception as e:
    print("Invalid request failed validation with exception:", e)

its-hammer-time avatar Mar 13 '25 16:03 its-hammer-time