[PR #597] Preserve filter HTTPException and return 429 in the rate limit example #482

Open
opened 2026-10-18 21:06:30 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/open-webui/pipelines/pull/597
Author: @sebnowak
Created: 3/16/2026
Status: 🔄 Open

Base: main ← Head: fix/preserve-filter-http-errors


📝 Commits (1)

  • 49c8d2f Preserve HTTPException in filter endpoints

📊 Changes

2 files changed (+9 additions, -1 deletions)

View changed files

📝 examples/filters/rate_limit_filter_pipeline.py (+5 -1)
📝 main.py (+4 -0)

📄 Description

I ran into this while testing the rate-limit filter pipeline through Open WebUI.

Currently, the filter endpoints in main.py wrap any exception as 500. That means a filter cannot intentionally return something like 429 and expect the caller to receive that status.

The rate-limit example also raises a plain Exception, so even when it is enforcing the limit, the response looks like an internal error instead of a real rate-limit response.

This patch keeps HTTPException intact in the filter endpoints and updates the rate-limit example to raise HTTPException(status_code=429, ...).

Scope of this PR:

  • preserve deliberate HTTP errors from filters
  • keep existing 500 behavior for unexpected exceptions
  • make the rate-limit example return 429 instead of a generic error

I have a matching Open WebUI PR (https://github.com/open-webui/open-webui/pull/22726).

I have personally tested the changes. How I tested:

  1. I ran belows python3 test_pipelines_http_errors.py (test creation was supported by Codex)
  2. That script executes the current source from this branch and verifies:
    • filter_inlet preserves a raised HTTPException(429, ...)
    • filter_outlet preserves a raised HTTPException(429, ...)
    • the bundled rate limit example raises a real 429 on the blocked request instead of a generic exception

CLI otuput:

Running Pipelines HTTP error checks...

test_filter_endpoints_preserve_http_exception (__main__.PipelinesHttpErrorTests.test_filter_endpoints_preserve_http_exception)
filter_inlet and filter_outlet preserve a raised HTTPException. ... ok
test_rate_limit_example_raises_http_429 (__main__.PipelinesHttpErrorTests.test_rate_limit_example_raises_http_429)
the bundled rate limit example returns HTTP 429 when blocked. ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.014s

OK

TEST

test_pipelines_http_errors.py

import ast
import asyncio
import contextlib
import io
import sys
import textwrap
import types
import unittest
from pathlib import Path


PIPELINES_ROOT = Path(__file__).resolve().parents[1] / "pipelines"


class HTTPException(Exception):
    def __init__(self, status_code: int, detail=None):
        super().__init__(detail)
        self.status_code = status_code
        self.detail = detail


class BaseModel:
    def __init__(self, **kwargs):
        for name, value in self.__class__.__dict__.items():
            if name.startswith("_") or callable(value):
                continue
            setattr(self, name, value)
        for key, value in kwargs.items():
            setattr(self, key, value)


class OpenAIChatMessage:
    pass


def _extract_async_function(path: Path, name: str) -> str:
    source = path.read_text(encoding="utf-8")
    tree = ast.parse(source)
    for node in ast.walk(tree):
        if isinstance(node, ast.AsyncFunctionDef) and node.name == name:
            return textwrap.dedent(ast.get_source_segment(source, node))
    raise ValueError(f"Could not find async function {name!r} in {path}")


class PipelinesHttpErrorTests(unittest.IsolatedAsyncioTestCase):
    async def test_filter_endpoints_preserve_http_exception(self):
        """filter_inlet and filter_outlet preserve a raised HTTPException."""
        main_path = PIPELINES_ROOT / "main.py"
        inlet_source = _extract_async_function(main_path, "filter_inlet")
        outlet_source = _extract_async_function(main_path, "filter_outlet")

        class FilterForm:
            def __init__(self, body, user=None):
                self.body = body
                self.user = user

        class RateLimitFilter:
            async def inlet(self, *_args, **_kwargs):
                raise HTTPException(429, "Rate limit exceeded. Please try again later.")

            async def outlet(self, *_args, **_kwargs):
                raise HTTPException(429, "Rate limit exceeded. Please try again later.")

        namespace = {
            "app": types.SimpleNamespace(
                state=types.SimpleNamespace(
                    PIPELINES={"rate-limit-filter": {"type": "filter"}}
                )
            ),
            "PIPELINE_MODULES": {"rate-limit-filter": RateLimitFilter()},
            "HTTPException": HTTPException,
            "status": types.SimpleNamespace(
                HTTP_404_NOT_FOUND=404,
                HTTP_500_INTERNAL_SERVER_ERROR=500,
            ),
            "FilterForm": FilterForm,
        }
        exec(inlet_source, namespace)
        exec(outlet_source, namespace)

        form = FilterForm(body={"model": "chat-model"}, user={"id": "user-1"})

        with self.assertRaises(HTTPException) as inlet_ctx:
            await namespace["filter_inlet"]("rate-limit-filter", form)
        self.assertEqual(inlet_ctx.exception.status_code, 429)

        with self.assertRaises(HTTPException) as outlet_ctx:
            await namespace["filter_outlet"]("rate-limit-filter", form)
        self.assertEqual(outlet_ctx.exception.status_code, 429)

    async def test_rate_limit_example_raises_http_429(self):
        """the bundled rate limit example returns HTTP 429 when blocked."""
        example_path = PIPELINES_ROOT / "examples/filters/rate_limit_filter_pipeline.py"
        source = example_path.read_text(encoding="utf-8")
        namespace = {
            "os": __import__("os"),
            "time": __import__("time"),
            "BaseModel": BaseModel,
            "OpenAIChatMessage": OpenAIChatMessage,
            "HTTPException": HTTPException,
            "List": __import__("typing").List,
            "Optional": __import__("typing").Optional,
        }
        fastapi_module = types.ModuleType("fastapi")
        fastapi_module.HTTPException = HTTPException
        pydantic_module = types.ModuleType("pydantic")
        pydantic_module.BaseModel = BaseModel
        schemas_module = types.ModuleType("schemas")
        schemas_module.OpenAIChatMessage = OpenAIChatMessage
        previous_modules = {
            name: sys.modules.get(name)
            for name in ("fastapi", "pydantic", "schemas")
        }
        sys.modules["fastapi"] = fastapi_module
        sys.modules["pydantic"] = pydantic_module
        sys.modules["schemas"] = schemas_module
        try:
            exec(source, namespace)
        finally:
            for name, module in previous_modules.items():
                if module is None:
                    sys.modules.pop(name, None)
                else:
                    sys.modules[name] = module
        pipeline = namespace["Pipeline"]()
        pipeline.valves.requests_per_minute = 1
        pipeline.valves.requests_per_hour = None
        pipeline.valves.sliding_window_limit = None
        user = {"id": "user-1", "role": "user"}

        with contextlib.redirect_stdout(io.StringIO()):
            await pipeline.inlet({"model": "chat-model"}, user)

        with self.assertRaises(HTTPException) as ctx:
            with contextlib.redirect_stdout(io.StringIO()):
                await pipeline.inlet({"model": "chat-model"}, user)

        self.assertEqual(ctx.exception.status_code, 429)
        self.assertEqual(
            ctx.exception.detail,
            "Rate limit exceeded. Please try again later.",
        )


if __name__ == "__main__":
    print("Running Pipelines HTTP error checks...\n", file=sys.stderr, flush=True)
    unittest.main(verbosity=2, buffer=True)


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/open-webui/pipelines/pull/597 **Author:** [@sebnowak](https://github.com/sebnowak) **Created:** 3/16/2026 **Status:** 🔄 Open **Base:** `main` ← **Head:** `fix/preserve-filter-http-errors` --- ### 📝 Commits (1) - [`49c8d2f`](https://github.com/open-webui/pipelines/commit/49c8d2f8843940f04b6856b32a693a2f6e493d89) Preserve HTTPException in filter endpoints ### 📊 Changes **2 files changed** (+9 additions, -1 deletions) <details> <summary>View changed files</summary> 📝 `examples/filters/rate_limit_filter_pipeline.py` (+5 -1) 📝 `main.py` (+4 -0) </details> ### 📄 Description I ran into this while testing the rate-limit filter pipeline through Open WebUI. Currently, the filter endpoints in `main.py` wrap any exception as `500`. That means a filter cannot intentionally return something like `429` and expect the caller to receive that status. The rate-limit example also raises a plain `Exception`, so even when it is enforcing the limit, the response looks like an internal error instead of a real rate-limit response. This patch keeps `HTTPException` intact in the filter endpoints and updates the rate-limit example to raise `HTTPException(status_code=429, ...)`. Scope of this PR: - preserve deliberate HTTP errors from filters - keep existing `500` behavior for unexpected exceptions - make the rate-limit example return `429` instead of a generic error I have a matching Open WebUI PR (https://github.com/open-webui/open-webui/pull/22726). I have personally tested the changes. How I tested: 1. I ran belows `python3 test_pipelines_http_errors.py` (test creation was supported by Codex) 2. That script executes the current source from this branch and verifies: - `filter_inlet` preserves a raised `HTTPException(429, ...)` - `filter_outlet` preserves a raised `HTTPException(429, ...)` - the bundled rate limit example raises a real `429` on the blocked request instead of a generic exception CLI otuput: ```text Running Pipelines HTTP error checks... test_filter_endpoints_preserve_http_exception (__main__.PipelinesHttpErrorTests.test_filter_endpoints_preserve_http_exception) filter_inlet and filter_outlet preserve a raised HTTPException. ... ok test_rate_limit_example_raises_http_429 (__main__.PipelinesHttpErrorTests.test_rate_limit_example_raises_http_429) the bundled rate limit example returns HTTP 429 when blocked. ... ok ---------------------------------------------------------------------- Ran 2 tests in 0.014s OK ``` ## TEST test_pipelines_http_errors.py ```python import ast import asyncio import contextlib import io import sys import textwrap import types import unittest from pathlib import Path PIPELINES_ROOT = Path(__file__).resolve().parents[1] / "pipelines" class HTTPException(Exception): def __init__(self, status_code: int, detail=None): super().__init__(detail) self.status_code = status_code self.detail = detail class BaseModel: def __init__(self, **kwargs): for name, value in self.__class__.__dict__.items(): if name.startswith("_") or callable(value): continue setattr(self, name, value) for key, value in kwargs.items(): setattr(self, key, value) class OpenAIChatMessage: pass def _extract_async_function(path: Path, name: str) -> str: source = path.read_text(encoding="utf-8") tree = ast.parse(source) for node in ast.walk(tree): if isinstance(node, ast.AsyncFunctionDef) and node.name == name: return textwrap.dedent(ast.get_source_segment(source, node)) raise ValueError(f"Could not find async function {name!r} in {path}") class PipelinesHttpErrorTests(unittest.IsolatedAsyncioTestCase): async def test_filter_endpoints_preserve_http_exception(self): """filter_inlet and filter_outlet preserve a raised HTTPException.""" main_path = PIPELINES_ROOT / "main.py" inlet_source = _extract_async_function(main_path, "filter_inlet") outlet_source = _extract_async_function(main_path, "filter_outlet") class FilterForm: def __init__(self, body, user=None): self.body = body self.user = user class RateLimitFilter: async def inlet(self, *_args, **_kwargs): raise HTTPException(429, "Rate limit exceeded. Please try again later.") async def outlet(self, *_args, **_kwargs): raise HTTPException(429, "Rate limit exceeded. Please try again later.") namespace = { "app": types.SimpleNamespace( state=types.SimpleNamespace( PIPELINES={"rate-limit-filter": {"type": "filter"}} ) ), "PIPELINE_MODULES": {"rate-limit-filter": RateLimitFilter()}, "HTTPException": HTTPException, "status": types.SimpleNamespace( HTTP_404_NOT_FOUND=404, HTTP_500_INTERNAL_SERVER_ERROR=500, ), "FilterForm": FilterForm, } exec(inlet_source, namespace) exec(outlet_source, namespace) form = FilterForm(body={"model": "chat-model"}, user={"id": "user-1"}) with self.assertRaises(HTTPException) as inlet_ctx: await namespace["filter_inlet"]("rate-limit-filter", form) self.assertEqual(inlet_ctx.exception.status_code, 429) with self.assertRaises(HTTPException) as outlet_ctx: await namespace["filter_outlet"]("rate-limit-filter", form) self.assertEqual(outlet_ctx.exception.status_code, 429) async def test_rate_limit_example_raises_http_429(self): """the bundled rate limit example returns HTTP 429 when blocked.""" example_path = PIPELINES_ROOT / "examples/filters/rate_limit_filter_pipeline.py" source = example_path.read_text(encoding="utf-8") namespace = { "os": __import__("os"), "time": __import__("time"), "BaseModel": BaseModel, "OpenAIChatMessage": OpenAIChatMessage, "HTTPException": HTTPException, "List": __import__("typing").List, "Optional": __import__("typing").Optional, } fastapi_module = types.ModuleType("fastapi") fastapi_module.HTTPException = HTTPException pydantic_module = types.ModuleType("pydantic") pydantic_module.BaseModel = BaseModel schemas_module = types.ModuleType("schemas") schemas_module.OpenAIChatMessage = OpenAIChatMessage previous_modules = { name: sys.modules.get(name) for name in ("fastapi", "pydantic", "schemas") } sys.modules["fastapi"] = fastapi_module sys.modules["pydantic"] = pydantic_module sys.modules["schemas"] = schemas_module try: exec(source, namespace) finally: for name, module in previous_modules.items(): if module is None: sys.modules.pop(name, None) else: sys.modules[name] = module pipeline = namespace["Pipeline"]() pipeline.valves.requests_per_minute = 1 pipeline.valves.requests_per_hour = None pipeline.valves.sliding_window_limit = None user = {"id": "user-1", "role": "user"} with contextlib.redirect_stdout(io.StringIO()): await pipeline.inlet({"model": "chat-model"}, user) with self.assertRaises(HTTPException) as ctx: with contextlib.redirect_stdout(io.StringIO()): await pipeline.inlet({"model": "chat-model"}, user) self.assertEqual(ctx.exception.status_code, 429) self.assertEqual( ctx.exception.detail, "Rate limit exceeded. Please try again later.", ) if __name__ == "__main__": print("Running Pipelines HTTP error checks...\n", file=sys.stderr, flush=True) unittest.main(verbosity=2, buffer=True) ``` --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
open-webui/pipelines#482
No description provided.