[
  {
    "path": ".github/DISCUSSION_TEMPLATE/issue-triage.yml",
    "content": "title: \"[Triage] \"\nlabels:\n  - triage\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to report an issue or suggest a feature!\n\n        **Before submitting, please:**\n        - Search [existing discussions](https://github.com/benoitc/gunicorn/discussions) and [issues](https://github.com/benoitc/gunicorn/issues) for duplicates\n        - Check the [FAQ](https://gunicorn.org/faq/) and [documentation](https://gunicorn.org/)\n\n  - type: dropdown\n    id: type\n    attributes:\n      label: Type\n      description: What type of issue is this?\n      options:\n        - Bug Report\n        - Feature Request\n        - Performance Issue\n        - Documentation Issue\n    validations:\n      required: true\n\n  - type: textarea\n    id: description\n    attributes:\n      label: Description\n      description: A clear description of the issue or feature request\n      placeholder: |\n        For bugs: What happened? What did you expect?\n        For features: What problem does this solve?\n    validations:\n      required: true\n\n  - type: textarea\n    id: reproduce\n    attributes:\n      label: Steps to Reproduce (for bugs)\n      description: Minimal steps to reproduce the behavior\n      placeholder: |\n        1. Create a simple app with...\n        2. Run gunicorn with...\n        3. Send request...\n        4. See error...\n    validations:\n      required: false\n\n  - type: textarea\n    id: config\n    attributes:\n      label: Configuration\n      description: Your gunicorn configuration (command line or config file)\n      render: bash\n      placeholder: |\n        gunicorn --workers 4 --bind 0.0.0.0:8000 myapp:app\n    validations:\n      required: false\n\n  - type: textarea\n    id: logs\n    attributes:\n      label: Logs / Error Output\n      description: Relevant logs or error messages (use --log-level debug for more detail)\n      render: text\n    validations:\n      required: false\n\n  - type: input\n    id: gunicorn-version\n    attributes:\n      label: Gunicorn Version\n      description: Output of `gunicorn --version`\n      placeholder: gunicorn 24.1.0\n    validations:\n      required: true\n\n  - type: input\n    id: python-version\n    attributes:\n      label: Python Version\n      description: Output of `python --version`\n      placeholder: Python 3.12.0\n    validations:\n      required: true\n\n  - type: dropdown\n    id: worker-class\n    attributes:\n      label: Worker Class\n      description: Which worker type are you using?\n      options:\n        - sync (default)\n        - gthread\n        - gevent\n        - eventlet\n        - tornado\n        - asgi (beta)\n        - custom\n        - N/A (feature request)\n    validations:\n      required: true\n\n  - type: input\n    id: os\n    attributes:\n      label: Operating System\n      description: Your OS and version\n      placeholder: Ubuntu 22.04, macOS 14.0, etc.\n    validations:\n      required: true\n\n  - type: textarea\n    id: additional\n    attributes:\n      label: Additional Context\n      description: Any other context (proxy setup, Docker, proposed solution, etc.)\n    validations:\n      required: false\n\n  - type: checkboxes\n    id: checklist\n    attributes:\n      label: Checklist\n      options:\n        - label: I have searched existing discussions and issues for duplicates\n          required: true\n        - label: I have checked the documentation and FAQ\n          required: true\n"
  },
  {
    "path": ".github/DISCUSSION_TEMPLATE/question.yml",
    "content": "title: \"[Question] \"\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Have a question about Gunicorn?\n\n        Before asking, please check:\n        - [Documentation](https://gunicorn.org/)\n        - [FAQ](https://gunicorn.org/faq/)\n        - [Settings Reference](https://gunicorn.org/reference/settings/)\n        - [Existing discussions](https://github.com/benoitc/gunicorn/discussions)\n\n  - type: textarea\n    id: question\n    attributes:\n      label: Question\n      description: What would you like to know?\n    validations:\n      required: true\n\n  - type: textarea\n    id: context\n    attributes:\n      label: Context\n      description: Any relevant context (your setup, what you've tried, etc.)\n      placeholder: |\n        I'm running gunicorn with...\n        I've tried...\n    validations:\n      required: false\n\n  - type: textarea\n    id: config\n    attributes:\n      label: Configuration (if relevant)\n      description: Your gunicorn configuration\n      render: bash\n    validations:\n      required: false\n\n  - type: checkboxes\n    id: checklist\n    attributes:\n      label: Checklist\n      options:\n        - label: I have checked the documentation and FAQ\n          required: true\n        - label: I have searched existing discussions\n          required: true\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: [benoitc]\nopen_collective: gunicorn\ncustom: [\"https://checkout.revolut.com/pay/c934e028-3a71-44eb-b99c-491342df2044\"]\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Bug Report / Feature Request\n    url: https://github.com/benoitc/gunicorn/discussions/new?category=issue-triage\n    about: Report a bug or request a feature (triaged before becoming an issue)\n  - name: Question\n    url: https://github.com/benoitc/gunicorn/discussions/new?category=q-a\n    about: Ask a question about configuration, deployment, or usage\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/preapproved.md",
    "content": "---\nname: Pre-Discussed and Approved Topics\nabout: Only for topics already discussed and approved in GitHub Discussions\ntitle: ''\nlabels: ''\nassignees: ''\n---\n\n**Only for topics already discussed and approved in the GitHub Discussions section.**\n\nDO NOT OPEN A NEW ISSUE. PLEASE USE THE DISCUSSIONS SECTION.\n\nLink to approved discussion:\n\n---\n\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"monthly\"\n"
  },
  {
    "path": ".github/workflows/docker-integration.yml",
    "content": "name: Docker Integration Tests\n\non:\n  push:\n    branches: [master]\n    paths:\n      - 'gunicorn/uwsgi/**'\n      - 'tests/docker/uwsgi/**'\n      - '.github/workflows/docker-integration.yml'\n  pull_request:\n    paths:\n      - 'gunicorn/uwsgi/**'\n      - 'tests/docker/uwsgi/**'\n      - '.github/workflows/docker-integration.yml'\n\npermissions:\n  contents: read\n\nenv:\n  FORCE_COLOR: 1\n\njobs:\n  uwsgi-nginx:\n    name: uWSGI Protocol with nginx\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Set up Python\n        uses: actions/setup-python@v6\n        with:\n          python-version: \"3.12\"\n          cache: pip\n          cache-dependency-path: requirements_test.txt\n\n      - name: Install test dependencies\n        run: |\n          python -m pip install --upgrade pip\n          python -m pip install pytest pytest-cov requests\n\n      - name: Run uWSGI integration tests\n        run: |\n          pytest tests/docker/uwsgi/ -v --tb=short\n"
  },
  {
    "path": ".github/workflows/docker-publish.yml",
    "content": "name: Docker Publish\non:\n  push:\n    tags:\n      - 'v*'\n      - '[0-9]+.[0-9]+.[0-9]+'\n  workflow_dispatch:\n\npermissions:\n  contents: read\n  packages: write\n\nenv:\n  REGISTRY: ghcr.io\n  IMAGE_NAME: ${{ github.repository }}\n\njobs:\n  build-and-push:\n    name: Build and Push Docker Image\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Log in to Container Registry\n        uses: docker/login-action@v3\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Extract metadata\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\n          tags: |\n            type=semver,pattern={{version}}\n            type=semver,pattern={{major}}.{{minor}}\n            type=semver,pattern={{major}}\n            type=raw,value=latest,enable={{is_default_branch}}\n\n      - name: Build and push\n        uses: docker/build-push-action@v6\n        with:\n          context: .\n          file: docker/Dockerfile\n          platforms: linux/amd64,linux/arm64\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n"
  },
  {
    "path": ".github/workflows/docs.yml",
    "content": "name: Docs\n\non:\n  push:\n    branches: [ master ]\n    paths:\n      - 'docs/**'\n      - 'mkdocs.yml'\n      - 'scripts/build_settings_doc.py'\n      - 'gunicorn/config.py'\n      - 'requirements_dev.txt'\n      - '.github/workflows/docs.yml'\n  pull_request:\n    paths:\n      - 'docs/**'\n      - 'mkdocs.yml'\n      - 'scripts/build_settings_doc.py'\n      - 'gunicorn/config.py'\n      - 'requirements_dev.txt'\n      - '.github/workflows/docs.yml'\n  workflow_dispatch:\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Set up Python\n        uses: actions/setup-python@v6\n        with:\n          python-version: '3.12'\n\n      - name: Install dependencies\n        run: |\n          python -m pip install --upgrade pip\n          pip install -e .\n          pip install -r requirements_dev.txt\n\n      - name: Build documentation\n        run: mkdocs build\n\n      - name: Upload site artifact\n        uses: actions/upload-artifact@v7\n        with:\n          name: gunicorn-site\n          path: site\n          retention-days: 7\n\n  deploy:\n    if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master'\n    needs: build\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Set up Python\n        uses: actions/setup-python@v6\n        with:\n          python-version: '3.12'\n\n      - name: Install dependencies\n        run: |\n          python -m pip install --upgrade pip\n          pip install -e .\n          pip install -r requirements_dev.txt\n\n      - name: Build documentation\n        run: mkdocs build\n\n      - name: Deploy to GitHub Pages\n        uses: peaceiris/actions-gh-pages@v4\n        with:\n          github_token: ${{ secrets.GITHUB_TOKEN }}\n          publish_dir: site\n          publish_branch: gh-pages\n          cname: gunicorn.org\n          commit_message: \"docs: deploy ${{ github.sha }}\"\n"
  },
  {
    "path": ".github/workflows/embedding-integration.yml",
    "content": "name: Embedding Service Integration Tests\n\non:\n  push:\n    paths:\n      - 'examples/embedding_service/**'\n      - 'gunicorn/dirty/**'\n  pull_request:\n    paths:\n      - 'examples/embedding_service/**'\n      - 'gunicorn/dirty/**'\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Build and start service\n        run: |\n          cd examples/embedding_service\n          docker compose up -d --build\n          docker compose logs -f &\n\n      - name: Wait for healthy\n        run: |\n          for i in {1..30}; do\n            curl -s http://127.0.0.1:8000/health && break\n            sleep 2\n          done\n\n      - name: Run tests\n        run: |\n          pip install requests numpy\n          python examples/embedding_service/test_embedding.py\n\n      - name: Cleanup\n        if: always()\n        run: |\n          cd examples/embedding_service\n          docker compose down\n"
  },
  {
    "path": ".github/workflows/freebsd.yml",
    "content": "name: FreeBSD\n\non:\n  push:\n  pull_request:\n  workflow_dispatch:\n\npermissions:\n  contents: read\n\nenv:\n  FORCE_COLOR: 1\n\njobs:\n  test:\n    name: FreeBSD ${{ matrix.freebsd-version }} / Python ${{ matrix.python-version }}\n    runs-on: ubuntu-latest\n    timeout-minutes: 30\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - freebsd-version: '14.2'\n            python-version: '3.12'\n            python-pkg: 'python312 py312-sqlite3'\n          - freebsd-version: '14.2'\n            python-version: '3.13'\n            python-pkg: 'python313 py313-sqlite3'\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Test on FreeBSD\n        uses: vmactions/freebsd-vm@v1\n        with:\n          release: ${{ matrix.freebsd-version }}\n          usesh: true\n          prepare: |\n            pkg install -y ${{ matrix.python-pkg }}\n          run: |\n            python${{ matrix.python-version }} -m venv venv\n            . venv/bin/activate\n            pip install --upgrade pip\n            pip install pytest pytest-cov pytest-asyncio coverage\n            pip install -e .\n            pytest --cov=gunicorn -v tests/ \\\n              --ignore=tests/workers/test_ggevent.py \\\n              --ignore=tests/workers/test_geventlet.py\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "content": "name: lint\non: [push, pull_request]\npermissions:\n  contents: read # to fetch code (actions/checkout)\nenv:\n  # note that some tools care only for the name, not the value\n  FORCE_COLOR: 1\njobs:\n  lint:\n    name: ${{ matrix.python-version }} / tox-${{ matrix.toxenv || '(other)' }}\n    timeout-minutes: 10\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        toxenv: [lint, pycodestyle]\n        python-version: [ \"3.12\" ]\n        include:\n          # for actions that want git env, not tox env\n          - toxenv: null\n            python-version: \"3.12\"\n    steps:\n      - uses: actions/checkout@v6\n      - name: Using Python ${{ matrix.python-version }}\n        uses: actions/setup-python@v6\n        with:\n          python-version: ${{ matrix.python-version }}\n          cache: pip\n      - name: Install Dependencies (tox)\n        if: ${{ matrix.toxenv }}\n        run: |\n          python -m pip install --upgrade pip\n          python -m pip install tox\n      - run: tox -e ${{ matrix.toxenv }}\n        if: ${{ matrix.toxenv }}\n      - name: Install Dependencies (non-toxic)\n        if: ${{ ! matrix.toxenv }}\n        run: |\n          python -m pip install --upgrade pip\n          python -m pip install -e .\n      - name: \"Check generated docs\"\n        if: ${{ ! matrix.toxenv }}\n        run: |\n          # Regenerate settings.md and check for uncommitted changes\n          python scripts/build_settings_doc.py\n          if unclean=$(git status --untracked-files=no --porcelain) && [ -z \"$unclean\" ]; then\n            echo \"no uncommitted changes in working tree (as it should be)\"\n          else\n            echo \"did you forget to run 'python scripts/build_settings_doc.py'?\"\n            echo \"$unclean\"\n            git diff\n            exit 2\n          fi\n"
  },
  {
    "path": ".github/workflows/tox.yml",
    "content": "name: tox\non: [push, pull_request]\npermissions:\n  contents: read # to fetch code (actions/checkout)\nenv:\n  # note that some tools care only for the name, not the value\n  FORCE_COLOR: 1\njobs:\n  tox:\n    name: ${{ matrix.os }} / ${{ matrix.python-version }}\n    # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes\n    timeout-minutes: 20\n    runs-on: ${{ matrix.os }}\n    strategy:\n      fail-fast: false\n      matrix:\n        unsupported: [false]\n        os:\n         - ubuntu-latest\n         # Not testing Windows: tests need Unix-only fcntl, grp, pwd, etc.\n         # FreeBSD: tested in separate freebsd.yml workflow\n        python-version:\n         # Supporting Python 3.10 through 3.13\n         - \"3.10\"\n         - \"3.11\"\n         - \"3.12\"\n         - \"3.13\"\n         - \"pypy-3.10\"\n        include:\n         # Test on macos-latest (arm64) with recent versions\n         - os: macos-latest\n           python-version: \"3.12\"\n           unsupported: false\n         - os: macos-latest\n           python-version: \"3.13\"\n           unsupported: false\n    steps:\n      - uses: actions/checkout@v6\n      - name: Using Python ${{ matrix.python-version }}\n        uses: actions/setup-python@v6\n        with:\n          python-version: ${{ matrix.python-version }}\n          cache: pip\n          cache-dependency-path: requirements_test.txt\n          check-latest: true\n          allow-prereleases: ${{ matrix.unsupported }}\n      - name: Install Dependencies\n        run: |\n          python -m pip install --upgrade pip\n          python -m pip install tox\n      - run: tox -e run-module\n        continue-on-error: ${{ matrix.unsupported }}\n      - run: tox -e run-entrypoint\n        continue-on-error: ${{ matrix.unsupported }}\n      - run: tox -e py\n        continue-on-error: ${{ matrix.unsupported }}\n"
  },
  {
    "path": ".gitignore",
    "content": "*.egg\n*.egg-info\n*.pyc\n*.so\n.coverage\n.pytest_cache\n.tox\n__pycache__\nbuild\ndocs/_build\ncoverage.xml\ndist\nexamples/frameworks/django/testing/testdb.sql\nexamples/frameworks/pylonstest/PasteScript*\nexamples/frameworks/pylonstest/pylonstest.egg-info/\nMANIFEST\nnohup.out\nsetuptools-*\nsite/\ndocs/site/\n"
  },
  {
    "path": ".pylintrc",
    "content": "[MASTER]\n\nignore=\n    build,\n    docs,\n    examples,\n    scripts,\n    _compat.py,\n    _gaiohttp.py,\n\n[MESSAGES CONTROL]\n\ndisable=\n    attribute-defined-outside-init,\n    bad-mcs-classmethod-argument,\n    bare-except,\n    broad-except,\n    cyclic-import,\n    duplicate-bases,\n    duplicate-code,\n    eval-used,\n    fixme,\n    import-error,\n    import-outside-toplevel,\n    import-self,\n    inconsistent-return-statements,\n    invalid-name,\n    missing-docstring,\n    no-else-return,\n    no-member,\n    no-self-argument,\n    no-staticmethod-decorator,\n    not-callable,\n    possibly-used-before-assignment,\n    protected-access,\n    raise-missing-from,\n    redefined-outer-name,\n    too-few-public-methods,\n    too-many-arguments,\n    too-many-branches,\n    too-many-instance-attributes,\n    too-many-lines,\n    too-many-locals,\n    too-many-nested-blocks,\n    too-many-positional-arguments,\n    too-many-public-methods,\n    too-many-statements,\n    used-before-assignment,\n    wrong-import-position,\n    wrong-import-order,\n    ungrouped-imports,\n    unused-argument,\n    useless-object-inheritance,\n    useless-import-alias,\n    comparison-with-callable,\n    try-except-raise,\n    consider-using-with,\n    consider-using-f-string,\n    unspecified-encoding\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Gunicorn\n\nWant to hack on Gunicorn? Awesome! Here are instructions to get you\nstarted. They are probably not perfect, please let us know if anything\nfeels wrong or incomplete.\n\n## Contribution guidelines\n\n### Pull requests are always welcome\n\nWe are always thrilled to receive pull requests, and do our best to\nprocess them as fast as possible. Not sure if that typo is worth a pull\nrequest? Do it! We will appreciate it.\n\nIf your pull request is not accepted on the first try, don't be\ndiscouraged! If there's a problem with the implementation, hopefully you\nreceived feedback on what to improve.\n\nWe're trying very hard to keep Gunicorn lean and focused. We don't want it\nto do everything for everybody. This means that we might decide against\nincorporating a new feature. However, there might be a way to implement\nthat feature *on top of* Gunicorn.\n\n### Start with a Discussion\n\nWe use [GitHub Discussions](https://github.com/benoitc/gunicorn/discussions)\nas the starting point for all bug reports, feature requests, and questions.\nThis allows for proper triage before creating formal issues.\n\n- **Bug reports**: Start in [Q&A](https://github.com/benoitc/gunicorn/discussions/categories/q-a)\n- **Feature requests**: Start in [Ideas](https://github.com/benoitc/gunicorn/discussions/categories/ideas)\n- **Questions**: Start in [Q&A](https://github.com/benoitc/gunicorn/discussions/categories/q-a)\n\nAfter discussion and triage, maintainers will create issues for confirmed\nbugs and approved features.\n\n### Check for existing discussions first!\n\nPlease take a moment to check that a discussion or issue doesn't already exist\ndocumenting your bug report or improvement proposal. If it does, it\nnever hurts to add a quick \"+1\" or \"I have this problem too\". This will\nhelp prioritize the most common problems and requests.\n\n\n### Conventions\n\nDon't comment on closed issues or PRs, instead open a new issue and link it to\nthe old one.\n\nFork the repo and make changes on your fork in a feature branch:\n\n- If it's a bugfix branch, name it XXX-something where XXX is the number\n  of the issue\n- If it's a feature branch, create an enhancement issue to announce your\n  intentions, and name it XXX-something where XXX is the number of the\nissue.\n\nSubmit unit tests for your changes. Python has a great test framework built\nin; use it! Take a look at existing tests for inspiration. Run the full\ntest suite on your branch before submitting a pull request.\n\nMake sure you include relevant updates or additions to documentation\nwhen creating or modifying features.\n\nIf you are adding a new configuration option or updating an existing one,\nplease do it in `gunicorn/config.py`, then run `make -C docs html` to update\n`docs/source/settings.rst`.\n\nWrite clean code.\n\nPull requests descriptions should be as clear as possible and include a\nreference to all the issues that they address.\n\nCode review comments may be added to your pull request. Discuss, then\nmake the suggested modifications and push additional commits to your\nfeature branch. Be sure to post a comment after pushing. The new commits\nwill show up in the pull request automatically, but the reviewers will\nnot be notified unless you comment.\n\nBefore the pull request is merged, make sure that you squash your\ncommits into logical units of work using `git rebase -i` and `git push\n-f`. After every commit the test suite should be passing. Include\ndocumentation changes in the same commit so that a revert would remove\nall traces of the feature or fix.\n\nCommits that fix or close an issue should include a reference like\n`Closes #XXX` or `Fixes #XXX`, which will automatically close the issue\nwhen merged.\n\nAdd your name to the THANKS file, but make sure the list is sorted and\nyour name and email address match your git configuration. The THANKS\nfile is regenerated occasionally from the git commit history, so a\nmismatch may result in your changes being overwritten.\n\n\n## Decision process\n\n### How are decisions made?\n\nShort answer: with pull requests to the gunicorn repository.\n\nGunicorn is an open-source project under the MIT License with an open\ndesign philosophy. This means that the repository is the source of truth\nfor EVERY aspect of the project, including its philosophy, design,\nroadmap and APIs. *If it's part of the project, it's in the repo. It's\nin the repo, it's part of the project.*\n\nAs a result, all decisions can be expressed as changes to the\nrepository. An implementation change is a change to the source code. An\nAPI change is a change to the API specification. A philosophy change is\na change to the relevant documentation. And so on.\n\nAll decisions affecting gunicorn, big and small, follow the same 3 steps:\n\n* Step 1: Open a pull request. Anyone can do this.\n\n* Step 2: Discuss the pull request. Anyone can do this.\n\n* Step 3: Accept or refuse a pull request. The relevant maintainer does this (see below \"Who decides what?\")\n\n\n### Who decides what?\n\nSo all decisions are pull requests, and the relevant maintainer makes\nthe decision by accepting or refusing the pull request.  But how do we\nidentify the relevant maintainer for a given pull request?\n\nGunicorn follows the timeless, highly efficient and totally unfair system\nknown as [Benevolent dictator for\nlife](http://en.wikipedia.org/wiki/Benevolent_Dictator_for_Life), with\nBenoit Chesneau (aka benoitc), in the role of BDFL.  This means that all\ndecisions are made by default by me. Since making every decision myself\nwould be highly unscalable, in practice decisions are spread across\nmultiple maintainers.\n\nThe relevant maintainer for a pull request is assigned in 3 steps:\n\n* Step 1: Determine the subdirectory affected by the pull request. This might be src/registry, docs/source/api, or any other part of the repo.\n\n* Step 2: Find the MAINTAINERS file which affects this directory. If the directory itself does not have a MAINTAINERS file, work your way up the repo hierarchy until you find one.\n\n* Step 3: The first maintainer listed is the primary maintainer who is assigned the Pull Request. The primary maintainer can reassign a Pull Request to other listed maintainers.\n\n\n### I'm a maintainer, should I make pull requests too?\n\nPrimary maintainers are not required to create pull requests when\nchanging their own subdirectory, but secondary maintainers are.\n\n### Who assigns maintainers?\n\nbenoitc.\n\n### How can I become a maintainer?\n\n* Step 1: learn the component inside out\n* Step 2: make yourself useful by contributing code, bugfixes, support etc.\n* Step 3: volunteer on our [Libera Chat](https://libera.chat/) irc channel [#gunicorn](https://web.libera.chat/?channels=#gunicorn)\n\nDon't forget: being a maintainer is a time investment. Make sure you\nwill have time to make yourself available.  You don't have to be a\nmaintainer to make a difference on the project!\n\n### What are a maintainer's responsibility?\n\nIt is every maintainer's responsibility to:\n\n* 1) Expose a clear roadmap for improving their component.\n* 2) Deliver prompt feedback and decisions on pull requests.\n* 3) Be available to anyone with questions, bug reports, criticism etc. on their component. This includes irc, github requests and the mailing list.\n* 4) Make sure their component respects the philosophy, design and roadmap of the project.\n\n### How is this process changed?\n\nJust like everything else: by making a pull request :)\n"
  },
  {
    "path": "LICENSE",
    "content": "2009-2026 (c) Benoît Chesneau <benoitc@gunicorn.org>\n2009-2015 (c) Paul J. Davis <paul.joseph.davis@gmail.com>\n\nPermission is hereby granted, free of charge, to any person\nobtaining a copy of this software and associated documentation\nfiles (the \"Software\"), to deal in the Software without\nrestriction, including without limitation the rights to use,\ncopy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\nOF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\nHOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nWHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\nOTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "MAINTAINERS",
    "content": "Core maintainers\n================\n\nBenoit Chesneau <benoitc@gunicorn.org>\nKonstantin Kapustin <sirkonst@gmail.com>\nRandall Leeds <randall.leeds@gmail.com>\nBerker Peksağ <berker.peksag@gmail.com>\nJason Madden <jason@nextthought.com>\nBrett Randall <javabrett@gmail.com>\n\nAlumni\n======\n\nThis list contains maintainers that are no longer active on the project.\nIt is thanks to these people that the project has become what it is today.\nThank you!\n\n\nPaul J. Davis <paul.joseph.davis@gmail.com>\nKenneth Reitz <me@kennethreitz.com>\nNikolay Kim <fafhrd91@gmail.com>\nAndrew Svetlov <andrew.svetlov@gmail.com>\nStéphane Wirtel <stephane@wirtel.be>"
  },
  {
    "path": "MANIFEST.in",
    "content": "include .gitignore\ninclude LICENSE\ninclude NOTICE\ninclude README.md\ninclude THANKS\ninclude requirements_dev.txt\ninclude requirements_test.txt\ninclude tox.ini\ninclude .pylintrc\nrecursive-include tests *\nrecursive-include examples *\nrecursive-include docs *\nrecursive-include examples/frameworks *\nrecursive-exclude * __pycache__\nrecursive-exclude docs/build *\nrecursive-exclude docs/_build *\nrecursive-exclude * *.py[co]\n"
  },
  {
    "path": "Makefile",
    "content": "build:\n\tvirtualenv venv\n\tvenv/bin/pip install -e .\n\tvenv/bin/pip install -r requirements_dev.txt\n\ndocs:\n\tmkdocs build\n\ndocs-serve:\n\tmkdocs serve\n\nclean:\n\t@rm -rf .Python MANIFEST build dist venv* *.egg-info *.egg\n\t@find . -type f -name \"*.py[co]\" -delete\n\t@find . -type d -name \"__pycache__\" -delete\n\n.PHONY: build clean docs docs-serve\n"
  },
  {
    "path": "NOTICE",
    "content": "Gunicorn\n\n2009-2026 (c) Benoît Chesneau <benoitc@gunicorn.org>\n2009-2015 (c) Paul J. Davis <paul.joseph.davis@gmail.com>\n\nGunicorn is released under the MIT license. See the LICENSE\nfile for the complete license.\n\ngunicorn.logging_config\n-----------------------\nCopyright 2001-2005 by Vinay Sajip. All Rights Reserved.\n\nPermission to use, copy, modify, and distribute this software and its\ndocumentation for any purpose and without fee is hereby granted,\nprovided that the above copyright notice appear in all copies and that\nboth that copyright notice and this permission notice appear in\nsupporting documentation, and that the name of Vinay Sajip\nnot be used in advertising or publicity pertaining to distribution\nof the software without specific, written prior permission.\n\nVINAY SAJIP DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,\nINCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL\nVINAY SAJIP BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR\nANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER\nIN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\nOF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\ngunicorn.debug\n--------------\n\nBased on eventlet.debug module under MIT license:\n\nUnless otherwise noted, the files in Eventlet are under the following MIT license:\n\nCopyright (c) 2005-2006, Bob Ippolito\nCopyright (c) 2007-2010, Linden Research, Inc.\nCopyright (c) 2008-2010, Eventlet Contributors (see Eventlet AUTHORS)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n\ngunicorn.reloader\n-----------------\n\nBased on greins.reloader module under MIT license:\n\n2010 (c) Meebo, Inc.\n\nPermission is hereby granted, free of charge, to any person\nobtaining a copy of this software and associated documentation\nfiles (the \"Software\"), to deal in the Software without\nrestriction, including without limitation the rights to use,\ncopy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\nOF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\nHOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nWHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\nOTHER DEALINGS IN THE SOFTWARE.\n\n\nutil/unlink.py\n--------------\n\nbackport from python3 Lib/test/support.py\n"
  },
  {
    "path": "README.md",
    "content": "# Gunicorn\n\n<p align=\"center\">\n  <strong>Gunicorn is maintained by volunteers. If it powers your production, please consider supporting us:</strong><br>\n  <a href=\"https://github.com/sponsors/benoitc\"><img src=\"https://img.shields.io/badge/GitHub_Sponsors-❤-ea4aaa?style=for-the-badge&logo=github\" alt=\"GitHub Sponsors\"></a>\n  <a href=\"https://opencollective.com/gunicorn\"><img src=\"https://img.shields.io/badge/Open_Collective-Support-7FADF2?style=for-the-badge&logo=opencollective\" alt=\"Open Collective\"></a>\n  <a href=\"https://checkout.revolut.com/pay/c934e028-3a71-44eb-b99c-491342df2044\"><img src=\"https://img.shields.io/badge/Revolut-Donate-191c20?style=for-the-badge\" alt=\"Revolut\"></a>\n</p>\n\n[![PyPI version](https://img.shields.io/pypi/v/gunicorn.svg?style=flat)](https://pypi.python.org/pypi/gunicorn)\n[![Supported Python versions](https://img.shields.io/pypi/pyversions/gunicorn.svg)](https://pypi.python.org/pypi/gunicorn)\n[![Build Status](https://github.com/benoitc/gunicorn/actions/workflows/tox.yml/badge.svg)](https://github.com/benoitc/gunicorn/actions/workflows/tox.yml)\n\nGunicorn 'Green Unicorn' is a Python WSGI HTTP Server for UNIX. It's a pre-fork\nworker model ported from Ruby's [Unicorn](https://bogomips.org/unicorn/) project. The Gunicorn server is broadly\ncompatible with various web frameworks, simply implemented, light on server\nresource usage, and fairly speedy.\n\n**New in v25**: Per-app worker allocation for dirty arbiters, HTTP/2 support (beta)!\n\n## Quick Start\n\n```bash\npip install gunicorn\ngunicorn myapp:app --workers 4\n```\n\nFor ASGI applications (FastAPI, Starlette):\n\n```bash\ngunicorn myapp:app --worker-class asgi\n```\n\n## Features\n\n- WSGI support for Django, Flask, Pyramid, and any WSGI framework\n- **ASGI support** for FastAPI, Starlette, Quart\n- **HTTP/2 support** (beta) with multiplexed streams\n- **Dirty Arbiters** (beta) for heavy workloads (ML models, long-running tasks)\n- uWSGI binary protocol for nginx integration\n- Multiple worker types: sync, gthread, gevent, eventlet, asgi\n- Graceful worker process management\n- Compatible with Python 3.9+\n\n## Documentation\n\nFull documentation at https://gunicorn.org\n\n- [Quickstart](https://gunicorn.org/quickstart/)\n- [Configuration](https://gunicorn.org/configure/)\n- [Deployment](https://gunicorn.org/deploy/)\n- [Settings Reference](https://gunicorn.org/reference/settings/)\n\n## Community\n\n- Report bugs on [GitHub Issues](https://github.com/benoitc/gunicorn/issues)\n- Chat in [#gunicorn](https://web.libera.chat/?channels=#gunicorn) on [Libera.chat](https://libera.chat/)\n- See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines\n\n## Support\n\nPowering Python apps since 2010. Support continued development.\n\n[![Become a Sponsor](https://img.shields.io/badge/Become_a_Sponsor-❤-ff69b4)](https://gunicorn.org/sponsor/)\n\n## License\n\nGunicorn is released under the MIT License. See the [LICENSE](https://github.com/benoitc/gunicorn/blob/master/LICENSE) file for details.\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Reporting a Vulnerability\n\n**Please note that public Github issues are open for everyone to see!**\n\nIf you believe you are found a problem in Gunicorn software, examples or documentation, we encourage you to send your\n report privately via [email](mailto:security@gunicorn.org?subject=Security%20issue%20in%20Gunicorn), or via Github\n using the *Report a vulnerability* button in the [Security](https://github.com/benoitc/gunicorn/security) section.\n\n## Supported Releases\n\nPlease target reports against :white_check_mark: or current master. Please understand that :x: will\n not receive further security attention.\n\n| Version | Status             |\n| ------- | ------------------ |\n| 25.0.0  | :white_check_mark: |\n| 24.1.1  | :white_check_mark: |\n| 23.0.0  | :x:                |\n| 22.0.0  | :x:                |\n| < 22.0  | :x:                |\n\n## Python Versions\n\nGunicorn runs on Python 3.10+, supporting Python versions that are still maintained by the PSF.\nWe *highly recommend* the latest release of a [supported series](https://devguide.python.org/versions/)\nand will not prioritize issues affecting EoL environments.\n"
  },
  {
    "path": "THANKS",
    "content": "Gunicorn THANKS\n===============\n\nA number of people have contributed to Gunicorn by reporting problems,\nsuggesting improvements or submitting changes. Some of these people are:\n\n414nch4n <chanfung032@gmail.com>\nAaron Kavlie <akavlie@gmail.com>\naartur <asiekielski@soldevelo.com>\nAdnane Belmadiaf <adnane002@gmail.com>\nAdrien CLERC <adrien@antipoul.fr>\nAlasdair Nicol <alasdair@thenicols.net>\nAlex Conrad <alexandre.conrad@gmail.com>\nAlex Gaynor <alex.gaynor@gmail.com>\nAlex Robbins <alexander.j.robbins@gmail.com>\nAlexandre Zani <alexandre.zani@gmail.com>\nAlexis Le-Quoc <alq@datadoghq.com>\nAnand Chitipothu <anandology@gmail.com>\nAndreas Stührk <andy-python@hammerhartes.de>\nAndrew Burdo <zeezooz@gmail.com>\nAndrew Svetlov <andrew.svetlov@gmail.com>\nAnil V <avaitla16@gmail.com>\nAntoine Girard <antoine.girard.dev@gmail.com>\nAnton Vlasenko <antares.spica@gmail.com>\nArtur Kruchinin <arturkruchinin@gmail.com>\nBartosz Oler <bartosz@bzimage.us>\nBen Cochran <bcochran@gmail.com>\nBen Oswald <ben.oswald@root-space.de>\nBenjamin Gilbert <bgilbert@backtick.net>\nBenny Mei <meibenny@gmail.com>\nBenoit Chesneau <bchesneau@gmail.com>\nBerker Peksag <berker.peksag@gmail.com>\nbninja <andrew@poundpay.com>\nBob Hagemann <bob+code@twilio.com>\nBobby Beckmann <bobby@macs-MacBook-Pro.local>\nBrett Randall <javabrett@gmail.com>\nBrian Rosner <brosner@gmail.com>\nBruno Bigras <bigras.bruno@gmail.com>\nCaleb Brown <git@calebbrown.id.au>\nChris Adams <chris@improbable.org>\nChris Forbes <chrisf@ijw.co.nz>\nChris Lamb <lamby@debian.org>\nChris Streeter <chris@chrisstreeter.com>\nChristian Clauss <cclauss@me.com>\nChristoph Heer <Christoph.Heer@gmail.com>\nChristos Stavrakakis <cstavr@grnet.gr>\nCMGS <ilskdw@mspil.edu.cn>\nCurt Micol <asenchi@asenchi.com>\nDan Callaghan <dcallagh@redhat.com>\nDan Sully <daniel-github@electricrain.com>\nDaniel Quinn <code@danielquinn.org>\nDariusz Suchojad <dsuch-github@m.zato.io>\nDavid Black <github@dhb.is>\nDavid Vincelli <david@freshbooks.com>\nDavid Wolever <david@wolever.net>\nDenis Bilenko <denis.bilenko@gmail.com>\nDiego Oliveira <contact@diegoholiveira.com>\nDima Barsky <github@kappa.ac93.org>\nDjoume Salvetti <djoume@freshbooks.com>\nDmitry Medvinsky <me@dmedvinsky.name>\nDominik Działak <ddzialak@users.noreply.github.com>\nDustin Ingram <di@users.noreply.github.com>\nEd Morley <edmorley@users.noreply.github.com>\nEric Florenzano <floguy@gmail.com>\nEric Shull <eric@elevenbasetwo.com>\nEugene Obukhov <irvind25@gmail.com>\nEvan Mezeske <evan@meebo-inc.com>\nFlorian Apolloner <florian@apolloner.eu>\nGaurav Kumar <gauravkumar37@gmail.com>\nGeorge Kollias <georgioskollias@gmail.com>\nGeorge Notaras <gnot@g-loaded.eu>\nGerman Larrain <germanlarrainm@gmail.com>\nGraham Dumpleton <Graham.Dumpleton@gmail.com>\nGraham Dumpleton <graham@newrelic.com>\nGreg McGuire <greg-github@greganem.com>\nGreg Taylor <gtaylor@duointeractive.com>\nHasan Ramezani <hasan.r67@gmail.com>\nHebert J <hebert@mail.ru>\nHobson Lane <shopper@totalgood.com>\nHugo van Kemenade <hugovk@users.noreply.github.com>\nIgor Petrov <igor.s.petrov@gmail.com>\nINADA Naoki <methane@users.noreply.github.com>\nJakub Paweł Głazik <zytek@nuxi.pl>\nJan-Philip Gehrcke <jgehrcke@googlemail.com>\nJannis Leidel <jannis@leidel.info>\nJason Jones <jcjones1515@users.noreply.github.com>\nJason Madden <jason@nextthought.com>\njean-philippe serafin <serafinjp@gmail.com>\nJeremy Volkman <jeremy@jvolkman.com>\nJeroen Pulles <jeroenp@users.noreply.github.com>\nJeryn Mathew <jerynmathew@gmail.com>\nJet Sun <jet.joins.sun@gmail.com>\nJim Garrison <jim@garrison.cc>\nJohan Bergström <bugs@bergstroem.nu>\nJohn Hensley <john@fairviewcomputing.com>\nJonas Haag <jonas@lophus.org>\nJonas Nockert <jonasnockert@gmail.com>\nJorge Niedbalski <jorge@nimbic.com>\nJorge Niedbalski R <niedbalski@gmail.com>\nJustin Quick <justquick@gmail.com>\nkeakon <keakon@gmail.com>\nKeegan Carruthers-Smith <keegan.csmith@gmail.com>\nKenneth Reitz <me@kennethreitz.org>\nKevin Gessner <kevin@kevingessner.com>\nKevin Littlejohn <kevin@littlejohn.id.au>\nKevin Luikens <kluikens@gmail.com>\nKirill Zaborsky <qrilka@gmail.com>\nKonstantin Kapustin <sirkonst@gmail.com>\nkracekumar <kracethekingmaker@gmail.com>\nKristian Glass <git@doismellburning.co.uk>\nKristian Øllegaard <kristian.ollegaard@divio.ch>\nKrystian <chrisjozwik@outlook.com>\nKrzysztof Urbaniak <urban@fail.pl>\nKyle Kelley <rgbkrk@gmail.com>\nKyle Mulka <repalviglator@yahoo.com>\nLars Hansson <romabysen@gmail.com>\nLeonardo Santagada <santagada@gmail.com>\nLevi Gross <levi@levigross.com>\nlicunlong <shenxiaogll@163.com>\nŁukasz Kucharski <lkucharski@leon.pl>\nMahmoud Hashemi <mahmoudrhashemi@gmail.com>\nMalthe Borch <mborch@gmail.com>\nMarc Abramowitz <marc@marc-abramowitz.com>\nMarc Abramowitz <msabramo@gmail.com>\nMark Adams <mark@markadams.me>\nMatt Behrens <askedrelic@gmail.com>\nMatt Billenstein <mattb@flingo.tv>\nMatt Good <matt@matt-good.net>\nMatt Robenolt <m@robenolt.com>\nMaxim Kamenkov <mkamenkov@gmail.com>\nMazdak Rezvani <mazdak@mac.com>\nMichael Schurter <m@schmichael.com>\nMieszko <mieszko.chowaniec@gmail.com>\nMike Tigas <mike@tig.as>\nMoriyoshi Koizumi <mozo@mozo.jp>\nmpaolini <markopaolini@gmail.com>\nNeil Chintomby <nchintomby@gmail.com>\nNeil Williams <neil@reddit.com>\nNick Pillitteri <nick@tshlabs.org>\nNik Nyby <nnyby@columbia.edu>\nNikolay Kim <fafhrd91@gmail.com>\nOliver Allen <oallenj@users.noreply.github.com>\nOliver Bristow <evilumbrella+github@gmail.com>\nOliver Tonnhofer <olt@bogosoft.com>\nOmer Katz <omer.drow@gmail.com>\nPA Parent <paparent@paparent.me>\nPaul Jeannot <paul.jeannot95@gmail.com>\nPaul Davis <davisp@neb.com>\nPaul J. Davis <paul.joseph.davis@gmail.com>\nPaul Smith <paulsmith@pobox.com>\nPhil Schanely <phil@daylife.com>\nPhilip Cristiano <philipcristiano@gmail.com>\nPhilipp Saveliev <fsfeel@gmail.com>\nPrateek Singh Paudel <pratykschingh@gmail.com>\npy <py@douban.com>\nQiangning Hong <hongqn@douban.com>\nRandall Leeds <randall.leeds@gmail.com>\nRandall Leeds <randall@bleeds.info>\nRandall Leeds <randall@meebo-inc.com>\nRaphaël Slinckx <rslinckx@gmail.com>\nRhys Powell <rhys@rhyspowell.com>\nRik <rvachterberg@gmail.com>\nRonan Amicel <ronan.amicel@gmail.com>\nRyan Peck <ryan@rypeck.com>\nRyuichi Watanabe <ryucrosskey@gmail.com>\nSaeed Gharedaghi <saeed.ghx68@gmail.com>\nSamuel Matos <samypr100@users.noreply.github.com>\nSergey Rublev <narma.nsk@gmail.com>\nShane Reustle <me@shanereustle.com>\nshouse-cars <shouse@cars.com>\nsib <andrew.sibley@gmail.com>\nSimon Lundmark <simon.lundmark@gmail.com>\nStephane Wirtel <stephane@wirtel.be>\nStephen DiCato <Locker537@gmail.com>\nStephen Holsapple <sholsapp@gmail.com>\nSteven Cummings <estebistec@gmail.com>\nsylt <sylt@users.noreply.github.com>\nSébastien Fievet <zyegfryed@gmail.com>\nTal Einat <532281+taleinat@users.noreply.github.com>\nTalha Malik <talham7391@hotmail.com>\nTedWantsMore <TedWantsMore@gmx.com>\nTeko012 <112829523+Teko012@users.noreply.github.com>\nThomas Grainger <tagrain@gmail.com>\nThomas Steinacher <tom@eggdrop.ch>\nTravis Cline <travis.cline@gmail.com>\nTravis Swicegood <development@domain51.com>\nTrey Long <trey@ktrl.com>\nW. Trevor King <wking@tremily.us>\nWojtek <wojtek@monodev.com>\nWolfgang Schnerring <wosc@wosc.de>\nWoLpH <Rick@Fawo.nl>\nwong2 <wonderfuly@gmail.com>\nWooParadog <guohaochuan@gmail.com>\nXie Shi <xieshi@douban.com>\nYue Du <ifduyue@gmail.com>\nzakdances <zakdances@gmail.com>\nEmile Fugulin <emilefugulin@hotmail.com>\n"
  },
  {
    "path": "appveyor.yml",
    "content": "version: '{branch}.{build}'\nenvironment:\n  matrix:\n    - TOXENV: lint\n      PYTHON: \"C:\\\\Python312-x64\"\n    - TOXENV: pycodestyle\n      PYTHON: \"C:\\\\Python312-x64\"\n    # Windows cannot even import the module when they unconditionally import, see below.\n    #- TOXENV: run-module\n    #  PYTHON: \"C:\\\\Python38-x64\"\n    #- TOXENV: run-entrypoint\n    #  PYTHON: \"C:\\\\Python38-x64\"\n    # Windows is not ready for testing!!!\n    # Python's fcntl, grp, pwd, os.geteuid(), and socket.AF_UNIX are all Unix-only.\n    #- TOXENV: py35\n    #  PYTHON: \"C:\\\\Python35-x64\"\n    #- TOXENV: py36\n    #  PYTHON: \"C:\\\\Python36-x64\"\n    #- TOXENV: py37\n    #  PYTHON: \"C:\\\\Python37-x64\"\n    #- TOXENV: py38\n    #  PYTHON: \"C:\\\\Python38-x64\"\n    #- TOXENV: py39\n    #  PYTHON: \"C:\\\\Python39-x64\"\n    #- TOXENV: py310\n    #  PYTHON: \"C:\\\\Python310-x64\"\n    #- TOXENV: py311\n    #  PYTHON: \"C:\\\\Python311-x64\"\n    #- TOXENV: py312\n    #  PYTHON: \"C:\\\\Python312-x64\"\nmatrix:\n  allow_failures:\n    # No failures expected for py312 and py313\ninit:\n  - SET \"PATH=%PYTHON%;%PYTHON%\\\\Scripts;%PATH%\"\ninstall:\n  - pip install tox\nbuild: false\ntest_script:\n  - tox\ncache:\n  # Not including the .tox directory since it takes longer to download/extract\n  # the cache archive than for tox to clean install from the pip cache.\n  - '%LOCALAPPDATA%\\pip\\Cache -> tox.ini'\nnotifications:\n  - provider: Email\n    on_build_success: false\n    on_build_status_changed: false\n"
  },
  {
    "path": "benchmarks/baseline.json",
    "content": "{\n  \"gthread\": {\n    \"simple\": {},\n    \"simple_high_concurrency\": {},\n    \"slow_io\": {},\n    \"large_response\": {}\n  }\n}"
  },
  {
    "path": "benchmarks/dirty_bench_app.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nBenchmark DirtyApp for stress testing the dirty arbiter pool.\n\nProvides configurable workloads for testing:\n- Pure sleep (scheduling overhead)\n- CPU-bound work (thread pool utilization)\n- Mixed I/O + CPU (realistic workloads)\n- Payload generation (serialization overhead)\n\"\"\"\n\nimport time\n\nfrom gunicorn.dirty import DirtyApp\n\n\nclass BenchmarkApp(DirtyApp):\n    \"\"\"\n    Configurable benchmark app for stress testing.\n\n    Provides various task types to test different aspects of the\n    dirty pool performance.\n    \"\"\"\n\n    def init(self):\n        \"\"\"Fast initialization - no heavy resources to load.\"\"\"\n        self.call_count = 0\n        self.total_sleep_ms = 0\n        self.total_cpu_ms = 0\n\n    def sleep_task(self, duration_ms):\n        \"\"\"\n        Pure sleep task - tests scheduling overhead.\n\n        This simulates I/O-bound work like waiting for external APIs.\n        The thread is blocked but not consuming CPU.\n\n        Args:\n            duration_ms: Sleep duration in milliseconds\n\n        Returns:\n            dict with sleep duration\n        \"\"\"\n        self.call_count += 1\n        self.total_sleep_ms += duration_ms\n        time.sleep(duration_ms / 1000.0)\n        return {\"slept_ms\": duration_ms}\n\n    def cpu_task(self, duration_ms, intensity=1.0):\n        \"\"\"\n        CPU-bound work - tests thread pool utilization.\n\n        Performs actual computation to simulate CPU-intensive work\n        like model inference or data processing.\n\n        Args:\n            duration_ms: Target duration in milliseconds\n            intensity: Work intensity multiplier (1.0 = normal)\n\n        Returns:\n            dict with computed iterations and actual duration\n        \"\"\"\n        self.call_count += 1\n        start = time.perf_counter()\n        target_end = start + (duration_ms / 1000.0)\n\n        # Perform CPU work until target duration\n        iterations = 0\n        work_per_iteration = int(1000 * intensity)\n\n        while time.perf_counter() < target_end:\n            # Do some actual computation\n            x = 0.0\n            for i in range(work_per_iteration):\n                x += i * 0.001\n                x = x * 1.001 if x < 1000000 else x * 0.999\n            iterations += 1\n\n        actual_ms = (time.perf_counter() - start) * 1000\n        self.total_cpu_ms += actual_ms\n\n        return {\n            \"iterations\": iterations,\n            \"target_ms\": duration_ms,\n            \"actual_ms\": round(actual_ms, 2),\n            \"intensity\": intensity\n        }\n\n    def mixed_task(self, sleep_ms, cpu_ms, intensity=1.0):\n        \"\"\"\n        Mixed I/O + CPU task - simulates realistic workloads.\n\n        First performs I/O (sleep), then does CPU work. This is\n        common in real apps: fetch data, then process it.\n\n        Args:\n            sleep_ms: I/O simulation duration in milliseconds\n            cpu_ms: CPU work duration in milliseconds\n            intensity: CPU work intensity multiplier\n\n        Returns:\n            dict with both sleep and CPU metrics\n        \"\"\"\n        self.call_count += 1\n\n        # I/O phase (sleep)\n        time.sleep(sleep_ms / 1000.0)\n        self.total_sleep_ms += sleep_ms\n\n        # CPU phase\n        start = time.perf_counter()\n        target_end = start + (cpu_ms / 1000.0)\n\n        iterations = 0\n        work_per_iteration = int(1000 * intensity)\n\n        while time.perf_counter() < target_end:\n            x = 0.0\n            for i in range(work_per_iteration):\n                x += i * 0.001\n                x = x * 1.001 if x < 1000000 else x * 0.999\n            iterations += 1\n\n        actual_cpu_ms = (time.perf_counter() - start) * 1000\n        self.total_cpu_ms += actual_cpu_ms\n\n        return {\n            \"sleep_ms\": sleep_ms,\n            \"cpu_iterations\": iterations,\n            \"target_cpu_ms\": cpu_ms,\n            \"actual_cpu_ms\": round(actual_cpu_ms, 2),\n            \"total_ms\": round(sleep_ms + actual_cpu_ms, 2)\n        }\n\n    def payload_task(self, size_bytes, duration_ms=0):\n        \"\"\"\n        Generate payload of specified size - tests serialization.\n\n        Creates a deterministic payload to test JSON serialization\n        overhead for different response sizes.\n\n        Args:\n            size_bytes: Target payload size in bytes\n            duration_ms: Optional sleep before generating payload\n\n        Returns:\n            dict with 'data' field of specified size\n        \"\"\"\n        self.call_count += 1\n\n        if duration_ms > 0:\n            time.sleep(duration_ms / 1000.0)\n            self.total_sleep_ms += duration_ms\n\n        # Generate payload - use a pattern that compresses differently\n        # than pure repeated characters for more realistic testing\n        pattern = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\"\n        repeats = (size_bytes // len(pattern)) + 1\n        data = (pattern * repeats)[:size_bytes]\n\n        return {\n            \"data\": data,\n            \"size\": len(data)\n        }\n\n    def echo_task(self, payload):\n        \"\"\"\n        Echo back payload - tests round-trip serialization.\n\n        Useful for testing request/response serialization together.\n\n        Args:\n            payload: Data to echo back\n\n        Returns:\n            dict with echoed payload and its size\n        \"\"\"\n        self.call_count += 1\n\n        # Calculate size based on type\n        if isinstance(payload, str):\n            size = len(payload)\n        elif isinstance(payload, (dict, list)):\n            import json\n            size = len(json.dumps(payload))\n        else:\n            size = len(str(payload))\n\n        return {\n            \"echoed_size\": size,\n            \"payload\": payload\n        }\n\n    def stats(self):\n        \"\"\"\n        Return accumulated statistics.\n\n        Returns:\n            dict with call counts and totals\n        \"\"\"\n        return {\n            \"call_count\": self.call_count,\n            \"total_sleep_ms\": self.total_sleep_ms,\n            \"total_cpu_ms\": round(self.total_cpu_ms, 2)\n        }\n\n    def reset_stats(self):\n        \"\"\"Reset accumulated statistics.\"\"\"\n        self.call_count = 0\n        self.total_sleep_ms = 0\n        self.total_cpu_ms = 0\n        return {\"reset\": True}\n\n    def health(self):\n        \"\"\"Health check endpoint for warmup.\"\"\"\n        return {\"status\": \"ok\"}\n\n    def close(self):\n        \"\"\"Cleanup on shutdown.\"\"\"\n        pass\n"
  },
  {
    "path": "benchmarks/dirty_bench_gunicorn.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nGunicorn configuration for dirty pool integration benchmarks.\n\nUsage:\n    gunicorn -c benchmarks/dirty_bench_gunicorn.py \\\n        benchmarks.dirty_bench_wsgi:app\n\"\"\"\n\n# Bind address\nbind = \"127.0.0.1:8000\"\n\n# HTTP worker configuration\nworkers = 4\nworker_class = \"gthread\"\nthreads = 4\nworker_connections = 1000\n\n# Dirty pool configuration\ndirty_apps = [\"benchmarks.dirty_bench_app:BenchmarkApp\"]\ndirty_workers = 4\ndirty_threads = 1\ndirty_timeout = 300\ndirty_graceful_timeout = 30\n\n# Logging\naccesslog = \"-\"\nerrorlog = \"-\"\nloglevel = \"info\"\n\n# Timeouts\ntimeout = 120\ngraceful_timeout = 30\nkeepalive = 2\n\n\n# Lifecycle hooks\n\ndef on_dirty_starting(arbiter):\n    \"\"\"Called when dirty arbiter is starting.\"\"\"\n    print(f\"[dirty] Arbiter starting (pid: {arbiter.pid})\")\n\n\ndef dirty_post_fork(arbiter, worker):\n    \"\"\"Called after dirty worker fork.\"\"\"\n    print(f\"[dirty] Worker {worker.pid} forked\")\n\n\ndef dirty_worker_init(worker):\n    \"\"\"Called after dirty worker apps are initialized.\"\"\"\n    print(f\"[dirty] Worker {worker.pid} initialized with apps: \"\n          f\"{list(worker.apps.keys())}\")\n\n\ndef dirty_worker_exit(arbiter, worker):\n    \"\"\"Called when dirty worker exits.\"\"\"\n    print(f\"[dirty] Worker {worker.pid} exiting\")\n"
  },
  {
    "path": "benchmarks/dirty_bench_wsgi.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nWSGI app for integration benchmarking of the dirty pool.\n\nThis simple WSGI application calls the dirty pool and returns results.\nUse with gunicorn for end-to-end benchmarking that includes HTTP overhead.\n\nExample:\n    gunicorn benchmarks.dirty_bench_wsgi:app \\\n        --workers 4 \\\n        --dirty-app benchmarks.dirty_bench_app:BenchmarkApp \\\n        --dirty-workers 2 \\\n        --bind 127.0.0.1:8000\n\"\"\"\n\nimport json\nfrom urllib.parse import parse_qs\n\nfrom gunicorn.dirty import get_dirty_client\n\n\n# Default benchmark app path\nBENCHMARK_APP = \"benchmarks.dirty_bench_app:BenchmarkApp\"\n\n\ndef app(environ, start_response):\n    \"\"\"\n    WSGI application that calls dirty pool tasks.\n\n    Query parameters:\n        action: Task action to call (default: sleep_task)\n        duration: Duration in ms for sleep/cpu tasks (default: 10)\n        sleep: Sleep duration for mixed_task (default: 50)\n        cpu: CPU duration for mixed_task (default: 50)\n        size: Payload size in bytes for payload_task (default: 100)\n        intensity: CPU intensity for cpu/mixed tasks (default: 1.0)\n        app: Dirty app path (default: benchmarks.dirty_bench_app:BenchmarkApp)\n\n    Endpoints:\n        /              - Default sleep_task\n        /sleep         - sleep_task with ?duration=N\n        /cpu           - cpu_task with ?duration=N&intensity=N\n        /mixed         - mixed_task with ?sleep=N&cpu=N\n        /payload       - payload_task with ?size=N\n        /echo          - echo_task (POST body echoed)\n        /stats         - Get accumulated stats\n        /health        - Health check\n    \"\"\"\n    path = environ.get('PATH_INFO', '/')\n    method = environ.get('REQUEST_METHOD', 'GET')\n    query = parse_qs(environ.get('QUERY_STRING', ''))\n\n    # Helper to get query params with defaults\n    def get_param(name, default, type_fn=int):\n        values = query.get(name, [])\n        if values:\n            try:\n                return type_fn(values[0])\n            except (ValueError, TypeError):\n                return default\n        return default\n\n    # Get app path from query or use default\n    app_path = query.get('app', [BENCHMARK_APP])[0]\n\n    try:\n        client = get_dirty_client()\n\n        # Route based on path\n        if path in ('/', '/sleep'):\n            duration = get_param('duration', 10)\n            result = client.execute(app_path, \"sleep_task\", duration)\n\n        elif path == '/cpu':\n            duration = get_param('duration', 100)\n            intensity = get_param('intensity', 1.0, float)\n            result = client.execute(app_path, \"cpu_task\", duration, intensity)\n\n        elif path == '/mixed':\n            sleep_ms = get_param('sleep', 50)\n            cpu_ms = get_param('cpu', 50)\n            intensity = get_param('intensity', 1.0, float)\n            result = client.execute(app_path, \"mixed_task\", sleep_ms, cpu_ms,\n                                    intensity)\n\n        elif path == '/payload':\n            size = get_param('size', 100)\n            duration = get_param('duration', 0)\n            result = client.execute(app_path, \"payload_task\", size, duration)\n\n        elif path == '/echo':\n            # Read request body for echo\n            try:\n                content_length = int(environ.get('CONTENT_LENGTH', 0))\n            except (ValueError, TypeError):\n                content_length = 0\n\n            if content_length > 0:\n                body = environ['wsgi.input'].read(content_length)\n                try:\n                    payload = json.loads(body.decode('utf-8'))\n                except (json.JSONDecodeError, UnicodeDecodeError):\n                    payload = body.decode('utf-8', errors='replace')\n            else:\n                payload = \"\"\n\n            result = client.execute(app_path, \"echo_task\", payload)\n\n        elif path == '/stats':\n            result = client.execute(app_path, \"stats\")\n\n        elif path == '/reset':\n            result = client.execute(app_path, \"reset_stats\")\n\n        elif path == '/health':\n            result = client.execute(app_path, \"health\")\n\n        else:\n            # Unknown path - return 404\n            status = '404 Not Found'\n            body = json.dumps({\"error\": f\"Unknown path: {path}\"}).encode()\n            headers = [\n                ('Content-Type', 'application/json'),\n                ('Content-Length', str(len(body))),\n            ]\n            start_response(status, headers)\n            return [body]\n\n        # Success response\n        status = '200 OK'\n        body = json.dumps(result).encode()\n        headers = [\n            ('Content-Type', 'application/json'),\n            ('Content-Length', str(len(body))),\n        ]\n        start_response(status, headers)\n        return [body]\n\n    except Exception as e:\n        # Error response\n        status = '500 Internal Server Error'\n        error_msg = {\"error\": str(e), \"type\": type(e).__name__}\n        body = json.dumps(error_msg).encode()\n        headers = [\n            ('Content-Type', 'application/json'),\n            ('Content-Length', str(len(body))),\n        ]\n        start_response(status, headers)\n        return [body]\n\n\n# Gunicorn configuration for integration testing\n# These can be overridden on the command line\n\n# Example gunicorn invocation:\n# gunicorn benchmarks.dirty_bench_wsgi:app \\\n#     -c benchmarks/dirty_bench_gunicorn.py \\\n#     --dirty-app benchmarks.dirty_bench_app:BenchmarkApp \\\n#     --dirty-workers 2\n\n\ndef post_fork(server, worker):\n    \"\"\"Hook called after worker fork.\"\"\"\n    pass\n"
  },
  {
    "path": "benchmarks/dirty_benchmark.py",
    "content": "#!/usr/bin/env python3\n#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nDirty Pool Benchmark Runner\n\nStress tests and benchmarks the dirty arbiter pool to find bottlenecks\nand optimization opportunities.\n\nTest Modes:\n- Isolated: Direct client -> arbiter -> worker (no HTTP overhead)\n- Integrated: HTTP workers calling dirty pool (realistic end-to-end)\n\nUsage:\n    # Quick smoke test\n    python benchmarks/dirty_benchmark.py --quick\n\n    # Full isolated suite\n    python benchmarks/dirty_benchmark.py --isolated --output results.json\n\n    # Specific scenario\n    python benchmarks/dirty_benchmark.py \\\n        --duration 100 \\\n        --concurrency 50 \\\n        --workers 4 \\\n        --threads 2\n\n    # Payload size tests\n    python benchmarks/dirty_benchmark.py --payload-tests\n\n    # Integration tests (requires gunicorn running)\n    python benchmarks/dirty_benchmark.py --integrated --url http://127.0.0.1:8000\n\"\"\"\n\nimport argparse\nimport asyncio\nimport json\nimport multiprocessing\nimport os\nimport signal\nimport statistics\nimport subprocess\nimport sys\nimport tempfile\nimport threading\nimport time\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\nfrom dataclasses import dataclass, field, asdict\nfrom pathlib import Path\nfrom typing import Any\n\n# Add parent to path for imports\nBENCHMARK_DIR = Path(__file__).parent\nsys.path.insert(0, str(BENCHMARK_DIR.parent))\n\nfrom gunicorn.dirty.client import DirtyClient\nfrom gunicorn.dirty.arbiter import DirtyArbiter\n\n\n# Default benchmark app path\nBENCHMARK_APP = \"benchmarks.dirty_bench_app:BenchmarkApp\"\n\n\n@dataclass\nclass LatencyStats:\n    \"\"\"Latency statistics in milliseconds.\"\"\"\n    min: float = 0.0\n    max: float = 0.0\n    mean: float = 0.0\n    stddev: float = 0.0\n    p50: float = 0.0\n    p95: float = 0.0\n    p99: float = 0.0\n\n    @classmethod\n    def from_samples(cls, samples: list[float]) -> \"LatencyStats\":\n        \"\"\"Calculate statistics from list of latency samples.\"\"\"\n        if not samples:\n            return cls()\n\n        sorted_samples = sorted(samples)\n        n = len(sorted_samples)\n\n        return cls(\n            min=sorted_samples[0],\n            max=sorted_samples[-1],\n            mean=statistics.mean(sorted_samples),\n            stddev=statistics.stdev(sorted_samples) if n > 1 else 0.0,\n            p50=sorted_samples[int(n * 0.50)],\n            p95=sorted_samples[int(n * 0.95)] if n >= 20 else sorted_samples[-1],\n            p99=sorted_samples[int(n * 0.99)] if n >= 100 else sorted_samples[-1],\n        )\n\n\n@dataclass\nclass BenchmarkResult:\n    \"\"\"Results from a single benchmark run.\"\"\"\n    scenario: str\n    config: dict\n    total_requests: int = 0\n    successful: int = 0\n    failed: int = 0\n    errors: list[str] = field(default_factory=list)\n    duration_sec: float = 0.0\n    requests_per_sec: float = 0.0\n    latency_ms: LatencyStats = field(default_factory=LatencyStats)\n\n    def to_dict(self) -> dict:\n        \"\"\"Convert to dictionary for JSON serialization.\"\"\"\n        d = asdict(self)\n        d['latency_ms'] = asdict(self.latency_ms)\n        return d\n\n\nclass MockConfig:\n    \"\"\"Mock gunicorn config for standalone arbiter testing.\"\"\"\n\n    def __init__(\n        self,\n        dirty_apps: list[str],\n        dirty_workers: int = 2,\n        dirty_threads: int = 1,\n        dirty_timeout: int = 300,\n        dirty_graceful_timeout: int = 30,\n    ):\n        self.dirty_apps = dirty_apps\n        self.dirty_workers = dirty_workers\n        self.dirty_threads = dirty_threads\n        self.dirty_timeout = dirty_timeout\n        self.dirty_graceful_timeout = dirty_graceful_timeout\n\n        # Other required config\n        self.env = {}\n        self.uid = os.getuid()\n        self.gid = os.getgid()\n        self.initgroups = False\n        self.proc_name = \"dirty-benchmark\"\n\n        # WorkerTmp requirements\n        self.umask = 0\n        self.worker_tmp_dir = None\n\n    # Hook stubs\n    def on_dirty_starting(self, arbiter):\n        pass\n\n    def dirty_post_fork(self, arbiter, worker):\n        pass\n\n    def dirty_worker_init(self, worker):\n        pass\n\n    def dirty_worker_exit(self, arbiter, worker):\n        pass\n\n\nclass MockLogger:\n    \"\"\"Mock logger for standalone testing.\"\"\"\n\n    def __init__(self, verbose: bool = False):\n        self.verbose = verbose\n\n    def debug(self, msg, *args):\n        if self.verbose:\n            print(f\"[DEBUG] {msg % args if args else msg}\")\n\n    def info(self, msg, *args):\n        if self.verbose:\n            print(f\"[INFO] {msg % args if args else msg}\")\n\n    def warning(self, msg, *args):\n        print(f\"[WARN] {msg % args if args else msg}\")\n\n    def error(self, msg, *args):\n        print(f\"[ERROR] {msg % args if args else msg}\")\n\n    def critical(self, msg, *args):\n        print(f\"[CRIT] {msg % args if args else msg}\")\n\n    def exception(self, msg, *args):\n        print(f\"[EXC] {msg % args if args else msg}\")\n\n    def reopen_files(self):\n        pass\n\n    def close_on_exec(self):\n        pass\n\n\nclass IsolatedBenchmark:\n    \"\"\"\n    Run benchmarks directly against the dirty pool without HTTP.\n\n    Spawns a standalone dirty arbiter and workers, then runs concurrent\n    clients to measure performance.\n    \"\"\"\n\n    def __init__(\n        self,\n        dirty_workers: int = 2,\n        dirty_threads: int = 1,\n        dirty_timeout: int = 300,\n        verbose: bool = False,\n    ):\n        self.dirty_workers = dirty_workers\n        self.dirty_threads = dirty_threads\n        self.dirty_timeout = dirty_timeout\n        self.verbose = verbose\n\n        self.arbiter = None\n        self.arbiter_pid = None\n        self.socket_path = None\n        self._tmpdir = None\n\n    def start(self):\n        \"\"\"Start the dirty arbiter and workers.\"\"\"\n        # Create temp directory for socket\n        self._tmpdir = tempfile.mkdtemp(prefix=\"dirty-bench-\")\n        self.socket_path = os.path.join(self._tmpdir, \"arbiter.sock\")\n\n        # Create config and logger\n        cfg = MockConfig(\n            dirty_apps=[BENCHMARK_APP],\n            dirty_workers=self.dirty_workers,\n            dirty_threads=self.dirty_threads,\n            dirty_timeout=self.dirty_timeout,\n        )\n        log = MockLogger(verbose=self.verbose)\n\n        # Fork arbiter process\n        pid = os.fork()\n        if pid == 0:\n            # Child process - run arbiter\n            try:\n                arbiter = DirtyArbiter(cfg, log, socket_path=self.socket_path)\n                arbiter.run()\n            except Exception as e:\n                print(f\"Arbiter error: {e}\")\n            finally:\n                os._exit(0)\n\n        # Parent process\n        self.arbiter_pid = pid\n\n        # Wait for arbiter socket to be ready\n        for _ in range(50):  # 5 seconds max\n            if os.path.exists(self.socket_path):\n                break\n            time.sleep(0.1)\n        else:\n            raise RuntimeError(\"Arbiter socket not ready\")\n\n        # Give workers time to start\n        time.sleep(0.5)\n\n    def stop(self):\n        \"\"\"Stop the dirty arbiter.\"\"\"\n        if self.arbiter_pid:\n            try:\n                os.kill(self.arbiter_pid, signal.SIGTERM)\n                os.waitpid(self.arbiter_pid, 0)\n            except (OSError, ChildProcessError):\n                pass\n            self.arbiter_pid = None\n\n        # Cleanup temp directory\n        if self._tmpdir:\n            try:\n                for f in os.listdir(self._tmpdir):\n                    os.unlink(os.path.join(self._tmpdir, f))\n                os.rmdir(self._tmpdir)\n            except OSError:\n                pass\n            self._tmpdir = None\n\n    def warmup(self, requests: int = 10):\n        \"\"\"Warm up the pool with a few requests.\"\"\"\n        with DirtyClient(self.socket_path, timeout=30.0) as client:\n            for _ in range(requests):\n                client.execute(BENCHMARK_APP, \"health\")\n\n    def run_benchmark(\n        self,\n        action: str,\n        args: tuple = (),\n        kwargs: dict = None,\n        total_requests: int = 1000,\n        concurrency: int = 10,\n        timeout: float = 30.0,\n    ) -> tuple[list[float], list[str]]:\n        \"\"\"\n        Run a benchmark with specified parameters.\n\n        Each concurrent worker maintains a persistent connection to the arbiter\n        and makes sequential requests. This simulates how real HTTP workers\n        use the dirty client (one connection per worker thread).\n\n        Args:\n            action: Action to call on the benchmark app\n            args: Positional arguments for the action\n            kwargs: Keyword arguments for the action\n            total_requests: Total number of requests to make\n            concurrency: Number of concurrent clients\n            timeout: Timeout per request in seconds\n\n        Returns:\n            Tuple of (latencies in ms, error messages)\n        \"\"\"\n        kwargs = kwargs or {}\n        latencies = []\n        errors = []\n        lock = threading.Lock()\n\n        # Calculate requests per worker\n        requests_per_worker = total_requests // concurrency\n        remainder = total_requests % concurrency\n\n        def worker_task(num_requests: int) -> None:\n            \"\"\"Worker that makes sequential requests on a persistent connection.\"\"\"\n            worker_latencies = []\n            worker_errors = []\n\n            try:\n                client = DirtyClient(self.socket_path, timeout=timeout)\n                client.connect()\n\n                for _ in range(num_requests):\n                    try:\n                        start = time.perf_counter()\n                        client.execute(BENCHMARK_APP, action, *args, **kwargs)\n                        elapsed = (time.perf_counter() - start) * 1000\n                        worker_latencies.append(elapsed)\n                    except Exception as e:\n                        worker_errors.append(str(e))\n                        # Reconnect on error\n                        try:\n                            client.close()\n                            client = DirtyClient(self.socket_path, timeout=timeout)\n                            client.connect()\n                        except Exception:\n                            pass\n\n                client.close()\n            except Exception as e:\n                worker_errors.append(f\"Connection error: {e}\")\n\n            # Add results to shared lists\n            with lock:\n                latencies.extend(worker_latencies)\n                errors.extend(worker_errors)\n\n        # Run concurrent workers\n        with ThreadPoolExecutor(max_workers=concurrency) as executor:\n            futures = []\n            for i in range(concurrency):\n                # Distribute remainder requests among first few workers\n                num = requests_per_worker + (1 if i < remainder else 0)\n                if num > 0:\n                    futures.append(executor.submit(worker_task, num))\n\n            # Wait for all workers to complete\n            for future in as_completed(futures):\n                future.result()  # Raises any exceptions\n\n        return latencies, errors\n\n\nclass IntegratedBenchmark:\n    \"\"\"\n    Run benchmarks against gunicorn with dirty pool via HTTP.\n\n    Uses wrk or ab for load testing, or falls back to Python requests.\n    \"\"\"\n\n    def __init__(\n        self,\n        url: str = \"http://127.0.0.1:8000\",\n        verbose: bool = False,\n    ):\n        self.url = url.rstrip('/')\n        self.verbose = verbose\n        self._tool = None\n\n    def check_dependencies(self) -> str | None:\n        \"\"\"Check for available load testing tools.\"\"\"\n        for tool in ['wrk', 'ab']:\n            try:\n                subprocess.run([tool, '--version'], capture_output=True,\n                               check=False)\n                return tool\n            except FileNotFoundError:\n                continue\n        return None\n\n    def warmup(self, requests: int = 10):\n        \"\"\"Warm up the server.\"\"\"\n        import urllib.request\n        for _ in range(requests):\n            try:\n                urllib.request.urlopen(f\"{self.url}/health\", timeout=5)\n            except Exception:\n                pass\n\n    def run_wrk(\n        self,\n        path: str,\n        duration: int = 10,\n        threads: int = 4,\n        connections: int = 100,\n    ) -> dict:\n        \"\"\"Run wrk benchmark and parse results.\"\"\"\n        url = f\"{self.url}{path}\"\n        cmd = [\n            'wrk',\n            '-t', str(threads),\n            '-c', str(connections),\n            '-d', f'{duration}s',\n            '--latency',\n            url,\n        ]\n\n        result = subprocess.run(cmd, capture_output=True, text=True,\n                                check=False)\n        return self._parse_wrk_output(result.stdout)\n\n    def _parse_wrk_output(self, output: str) -> dict:\n        \"\"\"Parse wrk output to extract metrics.\"\"\"\n        metrics = {\n            'requests_per_sec': 0.0,\n            'latency_ms': {},\n            'errors': 0,\n        }\n\n        for line in output.split('\\n'):\n            if 'Requests/sec' in line:\n                try:\n                    metrics['requests_per_sec'] = float(\n                        line.split(':')[1].strip())\n                except (ValueError, IndexError):\n                    pass\n            elif 'Latency' in line and 'Distribution' not in line:\n                parts = line.split()\n                if len(parts) >= 2:\n                    metrics['latency_ms']['avg'] = self._parse_duration(\n                        parts[1])\n            elif '50%' in line:\n                parts = line.split()\n                if len(parts) >= 2:\n                    metrics['latency_ms']['p50'] = self._parse_duration(\n                        parts[1])\n            elif '99%' in line:\n                parts = line.split()\n                if len(parts) >= 2:\n                    metrics['latency_ms']['p99'] = self._parse_duration(\n                        parts[1])\n            elif 'Socket errors' in line:\n                # Parse error counts\n                parts = line.split(',')\n                for part in parts:\n                    if any(x in part for x in ['connect', 'read', 'write',\n                                                 'timeout']):\n                        try:\n                            metrics['errors'] += int(part.split()[-1])\n                        except (ValueError, IndexError):\n                            pass\n\n        return metrics\n\n    def _parse_duration(self, s: str) -> float:\n        \"\"\"Parse wrk duration string (e.g., '12.34ms', '1.23s') to ms.\"\"\"\n        s = s.strip()\n        if s.endswith('us'):\n            return float(s[:-2]) / 1000\n        elif s.endswith('ms'):\n            return float(s[:-2])\n        elif s.endswith('s'):\n            return float(s[:-1]) * 1000\n        else:\n            return float(s)\n\n    def run_python_benchmark(\n        self,\n        path: str,\n        total_requests: int = 1000,\n        concurrency: int = 10,\n        timeout: float = 30.0,\n    ) -> tuple[list[float], list[str]]:\n        \"\"\"\n        Run benchmark using Python urllib.\n\n        Fallback when wrk/ab not available.\n        \"\"\"\n        import urllib.request\n        import urllib.error\n\n        url = f\"{self.url}{path}\"\n        latencies = []\n        errors = []\n\n        def make_request() -> tuple[float | None, str | None]:\n            try:\n                start = time.perf_counter()\n                urllib.request.urlopen(url, timeout=timeout)\n                elapsed = (time.perf_counter() - start) * 1000\n                return elapsed, None\n            except Exception as e:\n                return None, str(e)\n\n        with ThreadPoolExecutor(max_workers=concurrency) as executor:\n            futures = [executor.submit(make_request)\n                       for _ in range(total_requests)]\n\n            for future in as_completed(futures):\n                latency, error = future.result()\n                if latency is not None:\n                    latencies.append(latency)\n                if error:\n                    errors.append(error)\n\n        return latencies, errors\n\n\ndef run_isolated_suite(\n    workers: int = 2,\n    threads: int = 1,\n    verbose: bool = False,\n) -> list[BenchmarkResult]:\n    \"\"\"Run the full isolated benchmark suite.\"\"\"\n    results = []\n\n    bench = IsolatedBenchmark(\n        dirty_workers=workers,\n        dirty_threads=threads,\n        verbose=verbose,\n    )\n\n    print(f\"\\nStarting isolated benchmarks (workers={workers}, \"\n          f\"threads={threads})...\")\n\n    try:\n        bench.start()\n        bench.warmup()\n\n        # Define scenarios\n        scenarios = [\n            # Baseline\n            {\n                \"name\": \"baseline_10ms\",\n                \"action\": \"sleep_task\",\n                \"args\": (10,),\n                \"requests\": 1000,\n                \"concurrency\": 1,\n                \"description\": \"Single request latency (10ms sleep)\",\n            },\n            # Throughput\n            {\n                \"name\": \"throughput_10ms\",\n                \"action\": \"sleep_task\",\n                \"args\": (10,),\n                \"requests\": 5000,\n                \"concurrency\": 100,\n                \"description\": \"Max requests/sec (10ms sleep, 100 clients)\",\n            },\n            # CPU Bound\n            {\n                \"name\": \"cpu_bound_100ms\",\n                \"action\": \"cpu_task\",\n                \"args\": (100,),\n                \"requests\": 500,\n                \"concurrency\": 20,\n                \"description\": \"CPU-bound work (100ms, 20 clients)\",\n            },\n            # I/O Bound\n            {\n                \"name\": \"io_bound_500ms\",\n                \"action\": \"sleep_task\",\n                \"args\": (500,),\n                \"requests\": 200,\n                \"concurrency\": 50,\n                \"description\": \"I/O-bound work (500ms sleep, 50 clients)\",\n            },\n            # Mixed\n            {\n                \"name\": \"mixed_50_50\",\n                \"action\": \"mixed_task\",\n                \"args\": (50, 50),\n                \"requests\": 500,\n                \"concurrency\": 30,\n                \"description\": \"Mixed workload (50ms sleep + 50ms CPU)\",\n            },\n            # Overload\n            {\n                \"name\": \"overload_10ms\",\n                \"action\": \"sleep_task\",\n                \"args\": (10,),\n                \"requests\": 2000,\n                \"concurrency\": 200,\n                \"description\": \"Overload test (10ms, 200 clients)\",\n            },\n        ]\n\n        for scenario in scenarios:\n            print(f\"  Running {scenario['name']}: {scenario['description']}...\")\n\n            start_time = time.perf_counter()\n            latencies, errors = bench.run_benchmark(\n                action=scenario[\"action\"],\n                args=scenario.get(\"args\", ()),\n                kwargs=scenario.get(\"kwargs\"),\n                total_requests=scenario[\"requests\"],\n                concurrency=scenario[\"concurrency\"],\n            )\n            duration = time.perf_counter() - start_time\n\n            result = BenchmarkResult(\n                scenario=scenario[\"name\"],\n                config={\n                    \"dirty_workers\": workers,\n                    \"dirty_threads\": threads,\n                    \"task_action\": scenario[\"action\"],\n                    \"task_args\": scenario.get(\"args\", ()),\n                    \"concurrency\": scenario[\"concurrency\"],\n                },\n                total_requests=scenario[\"requests\"],\n                successful=len(latencies),\n                failed=len(errors),\n                errors=errors[:10] if errors else [],  # First 10 errors\n                duration_sec=round(duration, 2),\n                requests_per_sec=round(len(latencies) / duration, 1),\n                latency_ms=LatencyStats.from_samples(latencies),\n            )\n            results.append(result)\n\n            print(f\"    Requests/sec: {result.requests_per_sec:.1f}, \"\n                  f\"p50: {result.latency_ms.p50:.1f}ms, \"\n                  f\"p99: {result.latency_ms.p99:.1f}ms, \"\n                  f\"failed: {result.failed}\")\n\n    finally:\n        bench.stop()\n\n    return results\n\n\ndef run_payload_suite(\n    workers: int = 2,\n    threads: int = 1,\n    verbose: bool = False,\n) -> list[BenchmarkResult]:\n    \"\"\"Run payload size benchmark suite.\"\"\"\n    results = []\n\n    bench = IsolatedBenchmark(\n        dirty_workers=workers,\n        dirty_threads=threads,\n        verbose=verbose,\n    )\n\n    print(f\"\\nStarting payload benchmarks (workers={workers})...\")\n\n    try:\n        bench.start()\n        bench.warmup()\n\n        # Payload sizes to test\n        payload_sizes = [\n            (100, \"100B\", \"Tiny payload\"),\n            (1024, \"1KB\", \"Small payload\"),\n            (10240, \"10KB\", \"Medium payload\"),\n            (102400, \"100KB\", \"Large payload\"),\n            (1048576, \"1MB\", \"Very large payload\"),\n        ]\n\n        for size, size_label, description in payload_sizes:\n            # Adjust concurrency for larger payloads\n            concurrency = max(5, 100 // (size // 1024 + 1))\n            requests = max(100, 1000 // (size // 1024 + 1))\n\n            print(f\"  Running payload_{size_label}: {description}...\")\n\n            start_time = time.perf_counter()\n            latencies, errors = bench.run_benchmark(\n                action=\"payload_task\",\n                args=(size,),\n                total_requests=requests,\n                concurrency=concurrency,\n            )\n            duration = time.perf_counter() - start_time\n\n            result = BenchmarkResult(\n                scenario=f\"payload_{size_label}\",\n                config={\n                    \"dirty_workers\": workers,\n                    \"dirty_threads\": threads,\n                    \"payload_bytes\": size,\n                    \"concurrency\": concurrency,\n                },\n                total_requests=requests,\n                successful=len(latencies),\n                failed=len(errors),\n                errors=errors[:5] if errors else [],\n                duration_sec=round(duration, 2),\n                requests_per_sec=round(len(latencies) / duration, 1),\n                latency_ms=LatencyStats.from_samples(latencies),\n            )\n            results.append(result)\n\n            # Calculate throughput in MB/s\n            throughput_mb = (len(latencies) * size) / duration / 1024 / 1024\n\n            print(f\"    Requests/sec: {result.requests_per_sec:.1f}, \"\n                  f\"p50: {result.latency_ms.p50:.1f}ms, \"\n                  f\"throughput: {throughput_mb:.1f} MB/s\")\n\n    finally:\n        bench.stop()\n\n    return results\n\n\ndef run_quick_test(verbose: bool = False) -> list[BenchmarkResult]:\n    \"\"\"Run a quick smoke test.\"\"\"\n    results = []\n\n    bench = IsolatedBenchmark(dirty_workers=1, dirty_threads=1, verbose=verbose)\n\n    print(\"\\nRunning quick smoke test...\")\n\n    try:\n        bench.start()\n        bench.warmup(5)\n\n        # Simple test\n        start_time = time.perf_counter()\n        latencies, errors = bench.run_benchmark(\n            action=\"sleep_task\",\n            args=(10,),\n            total_requests=100,\n            concurrency=10,\n        )\n        duration = time.perf_counter() - start_time\n\n        result = BenchmarkResult(\n            scenario=\"quick_test\",\n            config={\"dirty_workers\": 1, \"dirty_threads\": 1},\n            total_requests=100,\n            successful=len(latencies),\n            failed=len(errors),\n            errors=errors[:5] if errors else [],\n            duration_sec=round(duration, 2),\n            requests_per_sec=round(len(latencies) / duration, 1),\n            latency_ms=LatencyStats.from_samples(latencies),\n        )\n        results.append(result)\n\n        print(f\"  Requests/sec: {result.requests_per_sec:.1f}, \"\n              f\"p50: {result.latency_ms.p50:.1f}ms, \"\n              f\"failed: {result.failed}\")\n\n        if result.failed == 0:\n            print(\"  PASS: Quick test successful\")\n        else:\n            print(f\"  WARN: {result.failed} requests failed\")\n\n    finally:\n        bench.stop()\n\n    return results\n\n\ndef run_config_sweep(verbose: bool = False) -> list[BenchmarkResult]:\n    \"\"\"\n    Sweep through different configurations to find optimal settings.\n\n    Tests combinations of workers and threads.\n    \"\"\"\n    results = []\n\n    configs = [\n        (1, 1),   # Baseline\n        (2, 1),   # 2 workers, 1 thread each\n        (4, 1),   # 4 workers, 1 thread each\n        (2, 2),   # 2 workers, 2 threads each\n        (2, 4),   # 2 workers, 4 threads each\n        (4, 2),   # 4 workers, 2 threads each\n    ]\n\n    print(\"\\nRunning configuration sweep...\")\n\n    for workers, threads in configs:\n        print(f\"\\n  Testing workers={workers}, threads={threads}...\")\n\n        bench = IsolatedBenchmark(\n            dirty_workers=workers,\n            dirty_threads=threads,\n            verbose=verbose,\n        )\n\n        try:\n            bench.start()\n            bench.warmup()\n\n            # Run a standard workload\n            start_time = time.perf_counter()\n            latencies, errors = bench.run_benchmark(\n                action=\"mixed_task\",\n                args=(20, 20),  # 20ms sleep + 20ms CPU\n                total_requests=1000,\n                concurrency=50,\n            )\n            duration = time.perf_counter() - start_time\n\n            result = BenchmarkResult(\n                scenario=f\"config_w{workers}_t{threads}\",\n                config={\n                    \"dirty_workers\": workers,\n                    \"dirty_threads\": threads,\n                    \"task\": \"mixed_task(20, 20)\",\n                    \"concurrency\": 50,\n                },\n                total_requests=1000,\n                successful=len(latencies),\n                failed=len(errors),\n                errors=errors[:5] if errors else [],\n                duration_sec=round(duration, 2),\n                requests_per_sec=round(len(latencies) / duration, 1),\n                latency_ms=LatencyStats.from_samples(latencies),\n            )\n            results.append(result)\n\n            print(f\"    Requests/sec: {result.requests_per_sec:.1f}, \"\n                  f\"p50: {result.latency_ms.p50:.1f}ms, \"\n                  f\"p99: {result.latency_ms.p99:.1f}ms\")\n\n        finally:\n            bench.stop()\n\n    # Print summary\n    print(\"\\n  Configuration Summary:\")\n    print(\"  \" + \"-\" * 60)\n    sorted_results = sorted(results, key=lambda r: -r.requests_per_sec)\n    for r in sorted_results:\n        cfg = r.config\n        print(f\"    w={cfg['dirty_workers']}, t={cfg['dirty_threads']}: \"\n              f\"{r.requests_per_sec:.1f} req/s, \"\n              f\"p99={r.latency_ms.p99:.1f}ms\")\n\n    return results\n\n\ndef generate_report(results: list[BenchmarkResult], output_path: str = None):\n    \"\"\"Generate a summary report from benchmark results.\"\"\"\n    print(\"\\n\" + \"=\" * 70)\n    print(\"BENCHMARK REPORT\")\n    print(\"=\" * 70)\n\n    for result in results:\n        print(f\"\\n{result.scenario}\")\n        print(\"-\" * 40)\n        print(f\"  Config: {json.dumps(result.config, indent=None)}\")\n        print(f\"  Requests: {result.successful}/{result.total_requests} \"\n              f\"({result.failed} failed)\")\n        print(f\"  Duration: {result.duration_sec}s\")\n        print(f\"  Throughput: {result.requests_per_sec:.1f} req/s\")\n        print(f\"  Latency (ms):\")\n        print(f\"    min: {result.latency_ms.min:.2f}\")\n        print(f\"    p50: {result.latency_ms.p50:.2f}\")\n        print(f\"    p95: {result.latency_ms.p95:.2f}\")\n        print(f\"    p99: {result.latency_ms.p99:.2f}\")\n        print(f\"    max: {result.latency_ms.max:.2f}\")\n        print(f\"    mean: {result.latency_ms.mean:.2f} \"\n              f\"(stddev: {result.latency_ms.stddev:.2f})\")\n\n        if result.errors:\n            print(f\"  Errors (first {len(result.errors)}):\")\n            for err in result.errors[:3]:\n                print(f\"    - {err[:80]}\")\n\n    if output_path:\n        output_data = {\n            \"timestamp\": time.strftime(\"%Y-%m-%dT%H:%M:%S\"),\n            \"results\": [r.to_dict() for r in results],\n        }\n        with open(output_path, 'w') as f:\n            json.dump(output_data, f, indent=2)\n        print(f\"\\nResults saved to: {output_path}\")\n\n\ndef main():\n    parser = argparse.ArgumentParser(\n        description='Benchmark the gunicorn dirty pool',\n        formatter_class=argparse.RawDescriptionHelpFormatter,\n        epilog=__doc__,\n    )\n\n    # Mode selection\n    mode_group = parser.add_mutually_exclusive_group()\n    mode_group.add_argument('--quick', action='store_true',\n                            help='Run quick smoke test')\n    mode_group.add_argument('--isolated', action='store_true',\n                            help='Run isolated benchmark suite')\n    mode_group.add_argument('--payload-tests', action='store_true',\n                            help='Run payload size tests')\n    mode_group.add_argument('--config-sweep', action='store_true',\n                            help='Sweep through configurations')\n    mode_group.add_argument('--integrated', action='store_true',\n                            help='Run integrated HTTP benchmarks')\n\n    # Configuration\n    parser.add_argument('--workers', type=int, default=2,\n                        help='Number of dirty workers (default: 2)')\n    parser.add_argument('--threads', type=int, default=1,\n                        help='Threads per dirty worker (default: 1)')\n    parser.add_argument('--duration', type=int, default=10,\n                        help='Task duration in ms for custom run')\n    parser.add_argument('--concurrency', type=int, default=10,\n                        help='Number of concurrent clients')\n    parser.add_argument('--requests', type=int, default=1000,\n                        help='Total requests to make')\n\n    # Integration mode options\n    parser.add_argument('--url', default='http://127.0.0.1:8000',\n                        help='Server URL for integrated tests')\n\n    # Output\n    parser.add_argument('--output', '-o',\n                        help='Output JSON file for results')\n    parser.add_argument('--verbose', '-v', action='store_true',\n                        help='Verbose output')\n\n    args = parser.parse_args()\n\n    results = []\n\n    try:\n        if args.quick:\n            results = run_quick_test(verbose=args.verbose)\n        elif args.isolated:\n            results = run_isolated_suite(\n                workers=args.workers,\n                threads=args.threads,\n                verbose=args.verbose,\n            )\n        elif args.payload_tests:\n            results = run_payload_suite(\n                workers=args.workers,\n                threads=args.threads,\n                verbose=args.verbose,\n            )\n        elif args.config_sweep:\n            results = run_config_sweep(verbose=args.verbose)\n        elif args.integrated:\n            bench = IntegratedBenchmark(url=args.url, verbose=args.verbose)\n            tool = bench.check_dependencies()\n\n            if tool == 'wrk':\n                print(f\"\\nRunning integrated benchmarks with wrk...\")\n                bench.warmup()\n\n                # Run basic scenarios\n                scenarios = [\n                    (\"/sleep?duration=10\", \"sleep_10ms\"),\n                    (\"/cpu?duration=100\", \"cpu_100ms\"),\n                    (\"/mixed?sleep=50&cpu=50\", \"mixed_50_50\"),\n                ]\n\n                for path, name in scenarios:\n                    print(f\"  Running {name}...\")\n                    metrics = bench.run_wrk(path, duration=10, connections=100)\n                    print(f\"    Requests/sec: {metrics.get('requests_per_sec', 'N/A')}\")\n\n                print(\"\\nNote: For detailed results, use wrk directly:\")\n                print(f\"  wrk -t4 -c100 -d30s --latency '{args.url}/sleep?duration=10'\")\n            else:\n                print(\"\\nUsing Python fallback (install wrk for better results)...\")\n                bench.warmup()\n\n                latencies, errors = bench.run_python_benchmark(\n                    \"/sleep?duration=10\",\n                    total_requests=args.requests,\n                    concurrency=args.concurrency,\n                )\n\n                result = BenchmarkResult(\n                    scenario=\"integrated_sleep\",\n                    config={\"url\": args.url, \"concurrency\": args.concurrency},\n                    total_requests=args.requests,\n                    successful=len(latencies),\n                    failed=len(errors),\n                    errors=errors[:5],\n                    duration_sec=sum(latencies) / 1000 / args.concurrency,\n                    requests_per_sec=len(latencies) / (sum(latencies) / 1000 /\n                                                        args.concurrency),\n                    latency_ms=LatencyStats.from_samples(latencies),\n                )\n                results.append(result)\n\n        else:\n            # Default: run custom single benchmark\n            print(f\"\\nRunning custom benchmark: \"\n                  f\"duration={args.duration}ms, concurrency={args.concurrency}\")\n\n            bench = IsolatedBenchmark(\n                dirty_workers=args.workers,\n                dirty_threads=args.threads,\n                verbose=args.verbose,\n            )\n\n            try:\n                bench.start()\n                bench.warmup()\n\n                start_time = time.perf_counter()\n                latencies, errors = bench.run_benchmark(\n                    action=\"sleep_task\",\n                    args=(args.duration,),\n                    total_requests=args.requests,\n                    concurrency=args.concurrency,\n                )\n                duration = time.perf_counter() - start_time\n\n                result = BenchmarkResult(\n                    scenario=\"custom\",\n                    config={\n                        \"dirty_workers\": args.workers,\n                        \"dirty_threads\": args.threads,\n                        \"task_duration_ms\": args.duration,\n                        \"concurrency\": args.concurrency,\n                    },\n                    total_requests=args.requests,\n                    successful=len(latencies),\n                    failed=len(errors),\n                    errors=errors[:10],\n                    duration_sec=round(duration, 2),\n                    requests_per_sec=round(len(latencies) / duration, 1),\n                    latency_ms=LatencyStats.from_samples(latencies),\n                )\n                results.append(result)\n\n            finally:\n                bench.stop()\n\n        # Generate report\n        if results:\n            generate_report(results, args.output)\n\n    except KeyboardInterrupt:\n        print(\"\\nBenchmark interrupted\")\n        sys.exit(1)\n    except Exception as e:\n        print(f\"\\nError: {e}\")\n        if args.verbose:\n            import traceback\n            traceback.print_exc()\n        sys.exit(1)\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "benchmarks/dirty_streaming.py",
    "content": "#!/usr/bin/env python\n#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nBenchmark suite for dirty worker streaming functionality.\n\nThis script benchmarks the streaming performance of dirty workers\nto measure throughput, latency, and memory usage.\n\nUsage:\n    python benchmarks/dirty_streaming.py [OPTIONS]\n\nOptions:\n    --quick     Run quick benchmarks only\n    --full      Run full benchmark suite including stress tests\n\"\"\"\n\nimport argparse\nimport asyncio\nimport gc\nimport json\nimport os\nimport struct\nimport sys\nimport time\nimport tracemalloc\nfrom datetime import datetime\nfrom unittest import mock\n\n# Add parent directory to path\nsys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))\n\nfrom gunicorn.dirty.protocol import (\n    DirtyProtocol,\n    make_request,\n    make_chunk_message,\n    make_end_message,\n    make_response,\n)\nfrom gunicorn.dirty.worker import DirtyWorker\nfrom gunicorn.dirty.arbiter import DirtyArbiter\nfrom gunicorn.dirty.client import (\n    DirtyClient,\n    DirtyStreamIterator,\n    DirtyAsyncStreamIterator,\n)\nfrom gunicorn.config import Config\n\n\nclass MockStreamWriter:\n    \"\"\"Mock StreamWriter that captures written messages.\"\"\"\n\n    def __init__(self):\n        self.messages = []\n        self._buffer = b\"\"\n        self.bytes_written = 0\n\n    def write(self, data):\n        self._buffer += data\n        self.bytes_written += len(data)\n\n    async def drain(self):\n        while len(self._buffer) >= DirtyProtocol.HEADER_SIZE:\n            length = struct.unpack(\n                DirtyProtocol.HEADER_FORMAT,\n                self._buffer[:DirtyProtocol.HEADER_SIZE]\n            )[0]\n            total_size = DirtyProtocol.HEADER_SIZE + length\n            if len(self._buffer) >= total_size:\n                msg_data = self._buffer[DirtyProtocol.HEADER_SIZE:total_size]\n                self._buffer = self._buffer[total_size:]\n                self.messages.append(DirtyProtocol.decode(msg_data))\n            else:\n                break\n\n    def close(self):\n        pass\n\n    async def wait_closed(self):\n        pass\n\n\nclass MockStreamReader:\n    \"\"\"Mock StreamReader that yields predefined messages.\"\"\"\n\n    def __init__(self, messages):\n        self._data = b''\n        for msg in messages:\n            self._data += DirtyProtocol.encode(msg)\n        self._pos = 0\n\n    async def readexactly(self, n):\n        if self._pos + n > len(self._data):\n            raise asyncio.IncompleteReadError(self._data[self._pos:], n)\n        result = self._data[self._pos:self._pos + n]\n        self._pos += n\n        return result\n\n\nclass MockLog:\n    \"\"\"Silent logger for benchmarks.\"\"\"\n\n    def debug(self, msg, *args):\n        pass\n\n    def info(self, msg, *args):\n        pass\n\n    def warning(self, msg, *args):\n        pass\n\n    def error(self, msg, *args):\n        pass\n\n    def close_on_exec(self):\n        pass\n\n    def reopen_files(self):\n        pass\n\n\ndef create_worker():\n    \"\"\"Create a test worker for benchmarks.\"\"\"\n    cfg = Config()\n    cfg.set(\"dirty_timeout\", 300)\n    log = MockLog()\n\n    with mock.patch('gunicorn.dirty.worker.WorkerTmp'):\n        worker = DirtyWorker(\n            age=1,\n            ppid=os.getpid(),\n            app_paths=[\"benchmark:App\"],\n            cfg=cfg,\n            log=log,\n            socket_path=\"/tmp/benchmark.sock\"\n        )\n\n    worker.apps = {}\n    worker._executor = None\n    worker.tmp = mock.Mock()\n\n    return worker\n\n\ndef create_arbiter():\n    \"\"\"Create a test arbiter for benchmarks.\"\"\"\n    cfg = Config()\n    cfg.set(\"dirty_timeout\", 300)\n    log = MockLog()\n\n    arbiter = DirtyArbiter(cfg=cfg, log=log)\n    arbiter.alive = True\n    arbiter.workers = {1234: mock.Mock()}\n    arbiter.worker_sockets = {1234: '/tmp/worker.sock'}\n\n    return arbiter\n\n\nclass BenchmarkResults:\n    \"\"\"Store and display benchmark results.\"\"\"\n\n    def __init__(self):\n        self.results = []\n\n    def add(self, name, iterations, duration, chunks=None, bytes_total=None,\n            memory_start=None, memory_end=None):\n        throughput = iterations / duration if duration > 0 else 0\n        result = {\n            \"name\": name,\n            \"iterations\": iterations,\n            \"duration_s\": round(duration, 4),\n            \"throughput_per_s\": round(throughput, 2),\n        }\n        if chunks:\n            result[\"chunks_per_s\"] = round(chunks / duration, 2)\n        if bytes_total:\n            result[\"mb_per_s\"] = round(bytes_total / (1024 * 1024) / duration, 2)\n        if memory_start is not None and memory_end is not None:\n            result[\"memory_start_mb\"] = round(memory_start / (1024 * 1024), 2)\n            result[\"memory_end_mb\"] = round(memory_end / (1024 * 1024), 2)\n            result[\"memory_delta_mb\"] = round((memory_end - memory_start) / (1024 * 1024), 2)\n        self.results.append(result)\n\n    def display(self):\n        print(\"\\n\" + \"=\" * 70)\n        print(\"BENCHMARK RESULTS\")\n        print(\"=\" * 70)\n        for result in self.results:\n            print(f\"\\n{result['name']}\")\n            print(\"-\" * 50)\n            for key, value in result.items():\n                if key != \"name\":\n                    print(f\"  {key}: {value}\")\n        print(\"\\n\" + \"=\" * 70)\n\n    def save_json(self, filepath):\n        with open(filepath, 'w') as f:\n            json.dump({\n                \"timestamp\": datetime.now().isoformat(),\n                \"results\": self.results\n            }, f, indent=2)\n        print(f\"Results saved to {filepath}\")\n\n\nasync def benchmark_worker_streaming_throughput(results, chunk_size=1024, num_chunks=1000):\n    \"\"\"Benchmark worker streaming throughput with various chunk sizes.\"\"\"\n    worker = create_worker()\n    writer = MockStreamWriter()\n\n    chunk_data = \"x\" * chunk_size\n\n    async def sync_gen():\n        for _ in range(num_chunks):\n            yield chunk_data\n\n    async def mock_execute(app_path, action, args, kwargs):\n        return sync_gen()\n\n    gc.collect()\n    tracemalloc.start()\n    memory_start = tracemalloc.get_traced_memory()[0]\n\n    start = time.perf_counter()\n\n    with mock.patch.object(worker, 'execute', side_effect=mock_execute):\n        request = make_request(\"bench-1\", \"benchmark:App\", \"stream\")\n        await worker.handle_request(request, writer)\n\n    duration = time.perf_counter() - start\n    memory_end = tracemalloc.get_traced_memory()[0]\n    tracemalloc.stop()\n\n    total_bytes = chunk_size * num_chunks\n\n    results.add(\n        f\"Worker streaming ({chunk_size}B chunks, {num_chunks} chunks)\",\n        iterations=1,\n        duration=duration,\n        chunks=num_chunks,\n        bytes_total=total_bytes,\n        memory_start=memory_start,\n        memory_end=memory_end\n    )\n\n\nasync def benchmark_arbiter_forwarding(results, num_chunks=1000):\n    \"\"\"Benchmark arbiter message forwarding throughput.\"\"\"\n    arbiter = create_arbiter()\n\n    messages = []\n    for i in range(num_chunks):\n        messages.append(make_chunk_message(f\"bench-{i}\", f\"data-{i}\"))\n    messages.append(make_end_message(f\"bench-{num_chunks}\"))\n\n    mock_reader = MockStreamReader(messages)\n\n    async def mock_get_connection(pid):\n        return mock_reader, MockStreamWriter()\n\n    arbiter._get_worker_connection = mock_get_connection\n\n    client_writer = MockStreamWriter()\n\n    gc.collect()\n    start = time.perf_counter()\n\n    request = make_request(\"bench-forward\", \"benchmark:App\", \"stream\")\n    await arbiter._execute_on_worker(1234, request, client_writer)\n\n    duration = time.perf_counter() - start\n\n    results.add(\n        f\"Arbiter forwarding ({num_chunks} chunks)\",\n        iterations=1,\n        duration=duration,\n        chunks=num_chunks,\n        bytes_total=client_writer.bytes_written\n    )\n\n    arbiter._cleanup_sync()\n\n\nasync def benchmark_streaming_latency(results, iterations=100):\n    \"\"\"Benchmark time-to-first-chunk and time-to-last-chunk.\"\"\"\n    worker = create_worker()\n\n    first_chunk_times = []\n    total_times = []\n\n    for _ in range(iterations):\n        writer = MockStreamWriter()\n\n        async def gen_3_chunks():\n            yield \"first\"\n            yield \"second\"\n            yield \"third\"\n\n        async def mock_execute(app_path, action, args, kwargs):\n            return gen_3_chunks()\n\n        start = time.perf_counter()\n\n        with mock.patch.object(worker, 'execute', side_effect=mock_execute):\n            request = make_request(\"bench-latency\", \"benchmark:App\", \"stream\")\n            await worker.handle_request(request, writer)\n\n            # Find time when first chunk was received\n            if writer.messages:\n                first_chunk_times.append(time.perf_counter() - start)\n\n        total_times.append(time.perf_counter() - start)\n\n    avg_first_chunk = sum(first_chunk_times) / len(first_chunk_times) if first_chunk_times else 0\n    avg_total = sum(total_times) / len(total_times)\n\n    print(f\"\\nLatency Results ({iterations} iterations):\")\n    print(f\"  Avg time-to-first-chunk: {avg_first_chunk * 1000:.3f}ms\")\n    print(f\"  Avg time-to-last-chunk: {avg_total * 1000:.3f}ms\")\n\n    results.add(\n        f\"Streaming latency ({iterations} iterations)\",\n        iterations=iterations,\n        duration=sum(total_times),\n        chunks=iterations * 3\n    )\n\n\nasync def benchmark_concurrent_streams(results, num_streams=10, chunks_per_stream=100):\n    \"\"\"Benchmark multiple concurrent streams.\"\"\"\n    arbiter = create_arbiter()\n\n    async def run_stream(stream_id):\n        messages = []\n        for i in range(chunks_per_stream):\n            messages.append(make_chunk_message(f\"stream-{stream_id}\", f\"chunk-{i}\"))\n        messages.append(make_end_message(f\"stream-{stream_id}\"))\n\n        mock_reader = MockStreamReader(messages)\n        async def mock_get_connection(pid):\n            return mock_reader, MockStreamWriter()\n\n        arbiter._get_worker_connection = mock_get_connection\n        client_writer = MockStreamWriter()\n\n        request = make_request(f\"bench-concurrent-{stream_id}\", \"benchmark:App\", \"stream\")\n        await arbiter._execute_on_worker(1234, request, client_writer)\n        return len(client_writer.messages)\n\n    gc.collect()\n    start = time.perf_counter()\n\n    # Run streams concurrently\n    tasks = [run_stream(i) for i in range(num_streams)]\n    results_list = await asyncio.gather(*tasks)\n\n    duration = time.perf_counter() - start\n\n    total_chunks = sum(results_list)\n\n    results.add(\n        f\"Concurrent streams ({num_streams} streams, {chunks_per_stream} chunks each)\",\n        iterations=num_streams,\n        duration=duration,\n        chunks=total_chunks\n    )\n\n    arbiter._cleanup_sync()\n\n\nasync def benchmark_memory_stability(results, iterations=10, chunks=1000):\n    \"\"\"Check memory stability over many iterations.\"\"\"\n    worker = create_worker()\n\n    gc.collect()\n    tracemalloc.start()\n    memory_samples = [tracemalloc.get_traced_memory()[0]]\n\n    for i in range(iterations):\n        writer = MockStreamWriter()\n\n        async def gen_chunks():\n            for j in range(chunks):\n                yield f\"chunk-{j}\"\n\n        async def mock_execute(app_path, action, args, kwargs):\n            return gen_chunks()\n\n        with mock.patch.object(worker, 'execute', side_effect=mock_execute):\n            request = make_request(f\"bench-mem-{i}\", \"benchmark:App\", \"stream\")\n            await worker.handle_request(request, writer)\n\n        gc.collect()\n        memory_samples.append(tracemalloc.get_traced_memory()[0])\n\n    tracemalloc.stop()\n\n    memory_start = memory_samples[0]\n    memory_end = memory_samples[-1]\n    memory_max = max(memory_samples)\n\n    print(f\"\\nMemory stability ({iterations} iterations of {chunks} chunks):\")\n    print(f\"  Start: {memory_start / 1024 / 1024:.2f}MB\")\n    print(f\"  End: {memory_end / 1024 / 1024:.2f}MB\")\n    print(f\"  Max: {memory_max / 1024 / 1024:.2f}MB\")\n    print(f\"  Delta: {(memory_end - memory_start) / 1024 / 1024:.2f}MB\")\n\n    results.add(\n        f\"Memory stability ({iterations} x {chunks} chunks)\",\n        iterations=iterations * chunks,\n        duration=0.001,  # Use small non-zero value to avoid division by zero\n        memory_start=memory_start,\n        memory_end=memory_end\n    )\n\n\nclass MockClientReader:\n    \"\"\"Mock async reader that simulates receiving streaming messages.\"\"\"\n\n    def __init__(self, num_chunks, chunk_data):\n        self.num_chunks = num_chunks\n        self.chunk_data = chunk_data\n        self._chunk_idx = 0\n        self._messages = []\n        self._build_messages()\n        self._pos = 0\n        self._data = b''\n        for msg in self._messages:\n            self._data += DirtyProtocol.encode(msg)\n\n    def _build_messages(self):\n        for i in range(self.num_chunks):\n            self._messages.append(make_chunk_message(f\"bench-{i}\", self.chunk_data))\n        self._messages.append(make_end_message(f\"bench-end\"))\n\n    async def readexactly(self, n):\n        if self._pos + n > len(self._data):\n            raise asyncio.IncompleteReadError(self._data[self._pos:], n)\n        result = self._data[self._pos:self._pos + n]\n        self._pos += n\n        return result\n\n\nclass MockClientWriter:\n    \"\"\"Mock async writer for client connection.\"\"\"\n\n    def __init__(self):\n        self._buffer = b\"\"\n        self._closed = False\n\n    def write(self, data):\n        self._buffer += data\n\n    async def drain(self):\n        pass\n\n    def close(self):\n        self._closed = True\n\n    async def wait_closed(self):\n        pass\n\n\nasync def benchmark_async_client_streaming(results, chunk_size=1024, num_chunks=1000):\n    \"\"\"\n    Benchmark DirtyAsyncStreamIterator directly.\n\n    Measures async iterator overhead vs raw message reading.\n    \"\"\"\n    chunk_data = \"x\" * chunk_size\n\n    # Create mock client with mock reader/writer\n    client = DirtyClient(\"/tmp/benchmark.sock\", timeout=30.0)\n    client._reader = MockClientReader(num_chunks, chunk_data)\n    client._writer = MockClientWriter()\n\n    gc.collect()\n    tracemalloc.start()\n    memory_start = tracemalloc.get_traced_memory()[0]\n\n    start = time.perf_counter()\n\n    # Use the async stream iterator directly\n    iterator = DirtyAsyncStreamIterator(client, \"benchmark:App\", \"stream\", (), {})\n    iterator._started = True  # Skip the request sending\n    iterator._request_id = \"bench-async\"\n    iterator._deadline = time.perf_counter() + 300  # 5 min deadline\n    iterator._last_chunk_time = time.perf_counter()\n\n    chunks_received = 0\n    bytes_received = 0\n    async for chunk in iterator:\n        chunks_received += 1\n        bytes_received += len(chunk)\n\n    duration = time.perf_counter() - start\n    memory_end = tracemalloc.get_traced_memory()[0]\n    tracemalloc.stop()\n\n    results.add(\n        f\"Async client streaming ({chunk_size}B chunks, {num_chunks} chunks)\",\n        iterations=1,\n        duration=duration,\n        chunks=chunks_received,\n        bytes_total=bytes_received,\n        memory_start=memory_start,\n        memory_end=memory_end\n    )\n\n\nasync def benchmark_sync_client_streaming(results, chunk_size=1024, num_chunks=1000):\n    \"\"\"\n    Benchmark DirtyStreamIterator directly (for comparison with async).\n\n    Note: This runs the sync iterator within an async context for comparison.\n    \"\"\"\n    chunk_data = \"x\" * chunk_size\n\n    # Build raw message data\n    messages_data = b''\n    for i in range(num_chunks):\n        msg = make_chunk_message(f\"bench-{i}\", chunk_data)\n        messages_data += DirtyProtocol.encode(msg)\n    messages_data += DirtyProtocol.encode(make_end_message(\"bench-end\"))\n\n    # Create a mock socket-like object\n    class MockSocket:\n        def __init__(self, data):\n            self._data = data\n            self._pos = 0\n            self._timeout = None\n\n        def recv(self, n, flags=0):\n            if self._pos >= len(self._data):\n                return b''\n            result = self._data[self._pos:self._pos + n]\n            self._pos += len(result)\n            return result\n\n        def settimeout(self, timeout):\n            self._timeout = timeout\n\n    # Create mock client\n    client = DirtyClient(\"/tmp/benchmark.sock\", timeout=30.0)\n    client._sock = MockSocket(messages_data)\n\n    gc.collect()\n    tracemalloc.start()\n    memory_start = tracemalloc.get_traced_memory()[0]\n\n    start = time.perf_counter()\n\n    # Use the sync stream iterator\n    iterator = DirtyStreamIterator(client, \"benchmark:App\", \"stream\", (), {})\n    iterator._started = True  # Skip the request sending\n    iterator._request_id = \"bench-sync\"\n    iterator._deadline = time.perf_counter() + 300  # 5 min deadline\n    iterator._last_chunk_time = time.perf_counter()\n\n    chunks_received = 0\n    bytes_received = 0\n    for chunk in iterator:\n        chunks_received += 1\n        bytes_received += len(chunk)\n\n    duration = time.perf_counter() - start\n    memory_end = tracemalloc.get_traced_memory()[0]\n    tracemalloc.stop()\n\n    results.add(\n        f\"Sync client streaming ({chunk_size}B chunks, {num_chunks} chunks)\",\n        iterations=1,\n        duration=duration,\n        chunks=chunks_received,\n        bytes_total=bytes_received,\n        memory_start=memory_start,\n        memory_end=memory_end\n    )\n\n\nasync def benchmark_async_vs_sync_client_streaming(results, chunk_size=1024, num_chunks=1000):\n    \"\"\"\n    Compare stream() vs stream_async() performance with the same workload.\n    \"\"\"\n    chunk_data = \"x\" * chunk_size\n\n    # --- Sync test ---\n    messages_data = b''\n    for i in range(num_chunks):\n        msg = make_chunk_message(f\"bench-{i}\", chunk_data)\n        messages_data += DirtyProtocol.encode(msg)\n    messages_data += DirtyProtocol.encode(make_end_message(\"bench-end\"))\n\n    class MockSocket:\n        def __init__(self, data):\n            self._data = data\n            self._pos = 0\n            self._timeout = None\n\n        def recv(self, n, flags=0):\n            if self._pos >= len(self._data):\n                return b''\n            result = self._data[self._pos:self._pos + n]\n            self._pos += len(result)\n            return result\n\n        def settimeout(self, timeout):\n            self._timeout = timeout\n\n    sync_client = DirtyClient(\"/tmp/benchmark.sock\", timeout=30.0)\n    sync_client._sock = MockSocket(messages_data)\n\n    gc.collect()\n    sync_start = time.perf_counter()\n\n    sync_iter = DirtyStreamIterator(sync_client, \"benchmark:App\", \"stream\", (), {})\n    sync_iter._started = True\n    sync_iter._request_id = \"bench-sync\"\n    sync_iter._deadline = time.perf_counter() + 300  # 5 min deadline\n    sync_iter._last_chunk_time = time.perf_counter()\n\n    sync_chunks = 0\n    for _ in sync_iter:\n        sync_chunks += 1\n\n    sync_duration = time.perf_counter() - sync_start\n\n    # --- Async test ---\n    async_client = DirtyClient(\"/tmp/benchmark.sock\", timeout=30.0)\n    async_client._reader = MockClientReader(num_chunks, chunk_data)\n    async_client._writer = MockClientWriter()\n\n    gc.collect()\n    async_start = time.perf_counter()\n\n    async_iter = DirtyAsyncStreamIterator(async_client, \"benchmark:App\", \"stream\", (), {})\n    async_iter._started = True\n    async_iter._request_id = \"bench-async\"\n    async_iter._deadline = time.perf_counter() + 300  # 5 min deadline\n    async_iter._last_chunk_time = time.perf_counter()\n\n    async_chunks = 0\n    async for _ in async_iter:\n        async_chunks += 1\n\n    async_duration = time.perf_counter() - async_start\n\n    # Report comparison\n    print(f\"\\nSync vs Async Client Streaming Comparison ({num_chunks} x {chunk_size}B chunks):\")\n    print(f\"  Sync:  {sync_duration * 1000:.3f}ms ({sync_chunks} chunks)\")\n    print(f\"  Async: {async_duration * 1000:.3f}ms ({async_chunks} chunks)\")\n    if sync_duration > 0:\n        ratio = async_duration / sync_duration\n        print(f\"  Ratio (async/sync): {ratio:.3f}x\")\n\n    results.add(\n        f\"Sync client streaming comparison ({chunk_size}B, {num_chunks} chunks)\",\n        iterations=1,\n        duration=sync_duration,\n        chunks=sync_chunks,\n        bytes_total=sync_chunks * chunk_size\n    )\n\n    results.add(\n        f\"Async client streaming comparison ({chunk_size}B, {num_chunks} chunks)\",\n        iterations=1,\n        duration=async_duration,\n        chunks=async_chunks,\n        bytes_total=async_chunks * chunk_size\n    )\n\n\nasync def run_quick_benchmarks():\n    \"\"\"Run quick benchmarks.\"\"\"\n    results = BenchmarkResults()\n\n    print(\"Running quick benchmarks...\")\n\n    await benchmark_worker_streaming_throughput(results, chunk_size=64, num_chunks=1000)\n    await benchmark_worker_streaming_throughput(results, chunk_size=1024, num_chunks=1000)\n    await benchmark_arbiter_forwarding(results, num_chunks=1000)\n    await benchmark_streaming_latency(results, iterations=50)\n\n    # Async client streaming benchmarks\n    await benchmark_async_client_streaming(results, chunk_size=1024, num_chunks=1000)\n    await benchmark_async_vs_sync_client_streaming(results, chunk_size=1024, num_chunks=1000)\n\n    return results\n\n\nasync def run_full_benchmarks():\n    \"\"\"Run full benchmark suite including stress tests.\"\"\"\n    results = BenchmarkResults()\n\n    print(\"Running full benchmark suite...\")\n\n    # Throughput tests with different chunk sizes\n    for chunk_size in [1, 64, 1024, 65536]:\n        await benchmark_worker_streaming_throughput(\n            results, chunk_size=chunk_size, num_chunks=1000\n        )\n\n    # Arbiter forwarding\n    await benchmark_arbiter_forwarding(results, num_chunks=10000)\n\n    # Latency\n    await benchmark_streaming_latency(results, iterations=100)\n\n    # Concurrent streams\n    await benchmark_concurrent_streams(results, num_streams=10, chunks_per_stream=100)\n    await benchmark_concurrent_streams(results, num_streams=50, chunks_per_stream=100)\n\n    # Memory stability\n    await benchmark_memory_stability(results, iterations=20, chunks=1000)\n\n    # Async client streaming benchmarks\n    for chunk_size in [64, 1024, 65536]:\n        await benchmark_async_client_streaming(results, chunk_size=chunk_size, num_chunks=1000)\n        await benchmark_sync_client_streaming(results, chunk_size=chunk_size, num_chunks=1000)\n\n    # Comparison benchmark\n    await benchmark_async_vs_sync_client_streaming(results, chunk_size=1024, num_chunks=5000)\n\n    return results\n\n\ndef main():\n    parser = argparse.ArgumentParser(description=\"Dirty streaming benchmarks\")\n    parser.add_argument(\"--quick\", action=\"store_true\", help=\"Run quick benchmarks only\")\n    parser.add_argument(\"--full\", action=\"store_true\", help=\"Run full benchmark suite\")\n    parser.add_argument(\"--output\", \"-o\", help=\"Output JSON file path\")\n    args = parser.parse_args()\n\n    if args.full:\n        results = asyncio.run(run_full_benchmarks())\n    else:\n        results = asyncio.run(run_quick_benchmarks())\n\n    results.display()\n\n    if args.output:\n        results.save_json(args.output)\n    else:\n        # Save to default location\n        output_dir = os.path.dirname(os.path.abspath(__file__))\n        results_dir = os.path.join(output_dir, \"results\")\n        os.makedirs(results_dir, exist_ok=True)\n        timestamp = datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n        output_file = os.path.join(results_dir, f\"streaming_benchmark_{timestamp}.json\")\n        results.save_json(output_file)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "benchmarks/quick_bench.sh",
    "content": "#!/bin/bash\n# Quick benchmark for gthread worker\n\nset -e\n\ncd \"$(dirname \"$0\")\"\n\necho \"Starting gunicorn with gthread worker...\"\n../.venv/bin/python -m gunicorn \\\n    --worker-class gthread \\\n    --workers 2 \\\n    --threads 4 \\\n    --worker-connections 1000 \\\n    --bind 127.0.0.1:8765 \\\n    --access-logfile /dev/null \\\n    --error-logfile /dev/null \\\n    --log-level warning \\\n    simple_app:application &\n\nGUNICORN_PID=$!\nsleep 3\n\necho \"\"\necho \"=== Benchmark: Simple requests (10000 requests, 100 concurrent) ===\"\nab -n 10000 -c 100 -k http://127.0.0.1:8765/ 2>&1 | grep -E \"(Requests per second|Time per request|Failed requests)\"\n\necho \"\"\necho \"=== Benchmark: High concurrency (5000 requests, 500 concurrent) ===\"\nab -n 5000 -c 500 -k http://127.0.0.1:8765/ 2>&1 | grep -E \"(Requests per second|Time per request|Failed requests)\"\n\necho \"\"\necho \"=== Benchmark: Large response (1000 requests, 50 concurrent) ===\"\nab -n 1000 -c 50 -k http://127.0.0.1:8765/large 2>&1 | grep -E \"(Requests per second|Time per request|Failed requests)\"\n\necho \"\"\necho \"Stopping gunicorn...\"\nkill $GUNICORN_PID 2>/dev/null || true\nwait $GUNICORN_PID 2>/dev/null || true\n\necho \"Done!\"\n"
  },
  {
    "path": "benchmarks/results/queue_refactor_results.json",
    "content": "{\n  \"timestamp\": \"2026-01-24T10:56:33\",\n  \"results\": [\n    {\n      \"scenario\": \"baseline_10ms\",\n      \"config\": {\n        \"dirty_workers\": 4,\n        \"dirty_threads\": 1,\n        \"task_action\": \"sleep_task\",\n        \"task_args\": [\n          10\n        ],\n        \"concurrency\": 1\n      },\n      \"total_requests\": 1000,\n      \"successful\": 1000,\n      \"failed\": 0,\n      \"errors\": [],\n      \"duration_sec\": 12.27,\n      \"requests_per_sec\": 81.5,\n      \"latency_ms\": {\n        \"min\": 10.432417009724304,\n        \"max\": 13.792542013106868,\n        \"mean\": 12.266892079642275,\n        \"stddev\": 0.871026700472873,\n        \"p50\": 12.80679099727422,\n        \"p95\": 13.078375020995736,\n        \"p99\": 13.141458010068163\n      }\n    },\n    {\n      \"scenario\": \"throughput_10ms\",\n      \"config\": {\n        \"dirty_workers\": 4,\n        \"dirty_threads\": 1,\n        \"task_action\": \"sleep_task\",\n        \"task_args\": [\n          10\n        ],\n        \"concurrency\": 100\n      },\n      \"total_requests\": 5000,\n      \"successful\": 5000,\n      \"failed\": 0,\n      \"errors\": [],\n      \"duration_sec\": 14.95,\n      \"requests_per_sec\": 334.4,\n      \"latency_ms\": {\n        \"min\": 11.470375000499189,\n        \"max\": 341.3927500077989,\n        \"mean\": 294.71728502821645,\n        \"stddev\": 34.9421432011074,\n        \"p50\": 305.2922079805285,\n        \"p95\": 326.4670000062324,\n        \"p99\": 334.32295799138956\n      }\n    },\n    {\n      \"scenario\": \"cpu_bound_100ms\",\n      \"config\": {\n        \"dirty_workers\": 4,\n        \"dirty_threads\": 1,\n        \"task_action\": \"cpu_task\",\n        \"task_args\": [\n          100\n        ],\n        \"concurrency\": 20\n      },\n      \"total_requests\": 500,\n      \"successful\": 500,\n      \"failed\": 0,\n      \"errors\": [],\n      \"duration_sec\": 12.55,\n      \"requests_per_sec\": 39.8,\n      \"latency_ms\": {\n        \"min\": 100.59350001392886,\n        \"max\": 502.4004160077311,\n        \"mean\": 493.9748328983551,\n        \"stddev\": 48.57073135808595,\n        \"p50\": 502.01483300770633,\n        \"p95\": 502.21283300197683,\n        \"p99\": 502.2801249870099\n      }\n    },\n    {\n      \"scenario\": \"io_bound_500ms\",\n      \"config\": {\n        \"dirty_workers\": 4,\n        \"dirty_threads\": 1,\n        \"task_action\": \"sleep_task\",\n        \"task_args\": [\n          500\n        ],\n        \"concurrency\": 50\n      },\n      \"total_requests\": 200,\n      \"successful\": 200,\n      \"failed\": 0,\n      \"errors\": [],\n      \"duration_sec\": 25.19,\n      \"requests_per_sec\": 7.9,\n      \"latency_ms\": {\n        \"min\": 501.3219590182416,\n        \"max\": 6563.243499986129,\n        \"mean\": 5566.4884116455505,\n        \"stddev\": 1566.1525736181566,\n        \"p50\": 6052.653749997262,\n        \"p95\": 6553.810708021047,\n        \"p99\": 6559.503666008823\n      }\n    },\n    {\n      \"scenario\": \"mixed_50_50\",\n      \"config\": {\n        \"dirty_workers\": 4,\n        \"dirty_threads\": 1,\n        \"task_action\": \"mixed_task\",\n        \"task_args\": [\n          50,\n          50\n        ],\n        \"concurrency\": 30\n      },\n      \"total_requests\": 500,\n      \"successful\": 500,\n      \"failed\": 0,\n      \"errors\": [],\n      \"duration_sec\": 12.98,\n      \"requests_per_sec\": 38.5,\n      \"latency_ms\": {\n        \"min\": 102.34933299943805,\n        \"max\": 839.0888340072706,\n        \"mean\": 756.4045974735054,\n        \"stddev\": 103.21897997316475,\n        \"p50\": 762.6495829899795,\n        \"p95\": 832.905125018442,\n        \"p99\": 836.0978330019861\n      }\n    },\n    {\n      \"scenario\": \"overload_10ms\",\n      \"config\": {\n        \"dirty_workers\": 4,\n        \"dirty_threads\": 1,\n        \"task_action\": \"sleep_task\",\n        \"task_args\": [\n          10\n        ],\n        \"concurrency\": 200\n      },\n      \"total_requests\": 2000,\n      \"successful\": 2000,\n      \"failed\": 0,\n      \"errors\": [],\n      \"duration_sec\": 5.99,\n      \"requests_per_sec\": 334.1,\n      \"latency_ms\": {\n        \"min\": 10.763874975964427,\n        \"max\": 625.4918330232613,\n        \"mean\": 565.1407622727129,\n        \"stddev\": 104.98938999734894,\n        \"p50\": 590.0453749927692,\n        \"p95\": 617.4105420068372,\n        \"p99\": 621.7636249784846\n      }\n    }\n  ]\n}"
  },
  {
    "path": "benchmarks/run_benchmark.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n#!/usr/bin/env python3\n\"\"\"\nBenchmark script for gunicorn gthread worker.\n\nThis script runs various benchmarks against gunicorn and reports performance metrics.\nRequires: gunicorn, requests (for warmup), and wrk or ab for load testing.\n\"\"\"\n\nimport argparse\nimport json\nimport os\nimport signal\nimport subprocess\nimport sys\nimport time\nfrom pathlib import Path\n\n\nBENCHMARK_DIR = Path(__file__).parent\nAPP_MODULE = \"simple_app:application\"\n\n\ndef check_dependencies():\n    \"\"\"Check if required tools are available.\"\"\"\n    # Check for wrk (preferred) or ab\n    for tool in ['wrk', 'ab']:\n        try:\n            subprocess.run([tool, '--version'], capture_output=True, check=False)\n            return tool\n        except FileNotFoundError:\n            continue\n    print(\"Error: Neither 'wrk' nor 'ab' found. Install one of them.\")\n    print(\"  macOS: brew install wrk\")\n    print(\"  Linux: apt-get install wrk (or apache2-utils for ab)\")\n    sys.exit(1)\n\n\ndef start_gunicorn(worker_class, workers, threads, connections, bind, extra_args=None):\n    \"\"\"Start gunicorn server and return the process.\"\"\"\n    cmd = [\n        sys.executable, '-m', 'gunicorn',\n        '--worker-class', worker_class,\n        '--workers', str(workers),\n        '--threads', str(threads),\n        '--worker-connections', str(connections),\n        '--bind', bind,\n        '--access-logfile', '-',\n        '--error-logfile', '-',\n        '--log-level', 'warning',\n        APP_MODULE,\n    ]\n    if extra_args:\n        cmd.extend(extra_args)\n\n    env = os.environ.copy()\n    env['PYTHONPATH'] = str(BENCHMARK_DIR.parent)\n\n    proc = subprocess.Popen(\n        cmd,\n        cwd=BENCHMARK_DIR,\n        env=env,\n        stdout=subprocess.PIPE,\n        stderr=subprocess.PIPE,\n    )\n\n    # Wait for server to be ready\n    time.sleep(2)\n    return proc\n\n\ndef stop_gunicorn(proc):\n    \"\"\"Stop the gunicorn server.\"\"\"\n    proc.send_signal(signal.SIGTERM)\n    try:\n        proc.wait(timeout=5)\n    except subprocess.TimeoutExpired:\n        proc.kill()\n        proc.wait()\n\n\ndef run_wrk_benchmark(url, duration, threads, connections):\n    \"\"\"Run wrk benchmark and return results.\"\"\"\n    cmd = [\n        'wrk',\n        '-t', str(threads),\n        '-c', str(connections),\n        '-d', f'{duration}s',\n        '--latency',\n        url,\n    ]\n    result = subprocess.run(cmd, capture_output=True, text=True, check=False)\n    return parse_wrk_output(result.stdout)\n\n\ndef run_ab_benchmark(url, requests, concurrency):\n    \"\"\"Run Apache Bench benchmark and return results.\"\"\"\n    cmd = [\n        'ab',\n        '-n', str(requests),\n        '-c', str(concurrency),\n        '-k',  # keepalive\n        url,\n    ]\n    result = subprocess.run(cmd, capture_output=True, text=True, check=False)\n    return parse_ab_output(result.stdout)\n\n\ndef parse_wrk_output(output):\n    \"\"\"Parse wrk output to extract metrics.\"\"\"\n    metrics = {}\n    for line in output.split('\\n'):\n        if 'Requests/sec' in line:\n            metrics['requests_per_sec'] = float(line.split(':')[1].strip())\n        elif 'Transfer/sec' in line:\n            metrics['transfer_per_sec'] = line.split(':')[1].strip()\n        elif 'Latency' in line and 'Distribution' not in line:\n            parts = line.split()\n            if len(parts) >= 2:\n                metrics['latency_avg'] = parts[1]\n        elif '50%' in line:\n            metrics['latency_p50'] = line.split()[1]\n        elif '99%' in line:\n            metrics['latency_p99'] = line.split()[1]\n    return metrics\n\n\ndef parse_ab_output(output):\n    \"\"\"Parse ab output to extract metrics.\"\"\"\n    metrics = {}\n    for line in output.split('\\n'):\n        if 'Requests per second' in line:\n            metrics['requests_per_sec'] = float(line.split(':')[1].split()[0])\n        elif 'Time per request' in line and 'mean' in line:\n            metrics['latency_avg'] = line.split(':')[1].strip()\n        elif 'Transfer rate' in line:\n            metrics['transfer_per_sec'] = line.split(':')[1].strip()\n    return metrics\n\n\ndef run_benchmark_suite(tool, bind_addr):\n    \"\"\"Run a suite of benchmarks.\"\"\"\n    results = {}\n\n    # Test configurations\n    configs = [\n        {'name': 'simple', 'path': '/', 'connections': 100},\n        {'name': 'simple_high_concurrency', 'path': '/', 'connections': 500},\n        {'name': 'slow_io', 'path': '/slow', 'connections': 50},\n        {'name': 'large_response', 'path': '/large', 'connections': 100},\n    ]\n\n    for config in configs:\n        url = f'http://{bind_addr}{config[\"path\"]}'\n        print(f\"  Running {config['name']}...\")\n\n        if tool == 'wrk':\n            metrics = run_wrk_benchmark(\n                url,\n                duration=10,\n                threads=4,\n                connections=config['connections'],\n            )\n        else:\n            metrics = run_ab_benchmark(\n                url,\n                requests=10000,\n                concurrency=config['connections'],\n            )\n\n        results[config['name']] = metrics\n        print(f\"    Requests/sec: {metrics.get('requests_per_sec', 'N/A')}\")\n\n    return results\n\n\ndef main():\n    parser = argparse.ArgumentParser(description='Benchmark gunicorn gthread worker')\n    parser.add_argument('--workers', type=int, default=2, help='Number of workers')\n    parser.add_argument('--threads', type=int, default=4, help='Threads per worker')\n    parser.add_argument('--connections', type=int, default=1000, help='Worker connections')\n    parser.add_argument('--bind', default='127.0.0.1:8000', help='Bind address')\n    parser.add_argument('--compare', action='store_true', help='Compare sync vs gthread')\n    parser.add_argument('--output', help='Output JSON file for results')\n    args = parser.parse_args()\n\n    tool = check_dependencies()\n    print(f\"Using benchmark tool: {tool}\")\n\n    all_results = {}\n\n    if args.compare:\n        # Compare sync and gthread workers\n        for worker_class in ['sync', 'gthread']:\n            print(f\"\\nBenchmarking {worker_class} worker...\")\n            proc = start_gunicorn(\n                worker_class=worker_class,\n                workers=args.workers,\n                threads=args.threads,\n                connections=args.connections,\n                bind=args.bind,\n            )\n            try:\n                all_results[worker_class] = run_benchmark_suite(tool, args.bind)\n            finally:\n                stop_gunicorn(proc)\n    else:\n        # Just benchmark gthread\n        print(\"\\nBenchmarking gthread worker...\")\n        proc = start_gunicorn(\n            worker_class='gthread',\n            workers=args.workers,\n            threads=args.threads,\n            connections=args.connections,\n            bind=args.bind,\n        )\n        try:\n            all_results['gthread'] = run_benchmark_suite(tool, args.bind)\n        finally:\n            stop_gunicorn(proc)\n\n    # Print summary\n    print(\"\\n\" + \"=\" * 60)\n    print(\"BENCHMARK SUMMARY\")\n    print(\"=\" * 60)\n    for worker, results in all_results.items():\n        print(f\"\\n{worker.upper()} Worker:\")\n        for test, metrics in results.items():\n            rps = metrics.get('requests_per_sec', 'N/A')\n            print(f\"  {test}: {rps} req/s\")\n\n    if args.output:\n        with open(args.output, 'w') as f:\n            json.dump(all_results, f, indent=2)\n        print(f\"\\nResults saved to {args.output}\")\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "benchmarks/simple_app.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n# Simple WSGI app for benchmarking\n\ndef application(environ, start_response):\n    \"\"\"Basic hello world response.\"\"\"\n    path = environ.get('PATH_INFO', '/')\n\n    if path == '/large':\n        body = b'X' * 65536  # 64KB\n    else:\n        body = b'Hello, World!'\n\n    status = '200 OK'\n    headers = [\n        ('Content-Type', 'text/plain'),\n        ('Content-Length', str(len(body))),\n    ]\n    start_response(status, headers)\n    return [body]\n"
  },
  {
    "path": "docker/.dockerignore",
    "content": ".git\n.github\n__pycache__\n*.pyc\n.pytest_cache\n.tox\ndocs\ntests\n"
  },
  {
    "path": "docker/Dockerfile",
    "content": "FROM python:3.12-slim\n\nLABEL org.opencontainers.image.source=https://github.com/benoitc/gunicorn\nLABEL org.opencontainers.image.description=\"Gunicorn Python WSGI HTTP Server\"\nLABEL org.opencontainers.image.licenses=MIT\n\n# Create non-root user\nRUN useradd --create-home --shell /bin/bash gunicorn\n\nWORKDIR /app\n\n# Install gunicorn from source\nCOPY pyproject.toml README.md LICENSE ./\nCOPY gunicorn/ ./gunicorn/\nRUN pip install --no-cache-dir .\n\n# Copy entrypoint script\nCOPY docker/docker-entrypoint.sh /usr/local/bin/\nRUN chmod +x /usr/local/bin/docker-entrypoint.sh\n\n# Configuration via environment:\n#   GUNICORN_BIND    - full bind address (default: 0.0.0.0:8000)\n#   GUNICORN_HOST    - bind host (default: 0.0.0.0)\n#   GUNICORN_PORT    - bind port (default: 8000)\n#   GUNICORN_WORKERS - number of workers (default: 2 * CPU + 1)\n#   GUNICORN_ARGS    - additional arguments (e.g., \"--timeout 120\")\n\nUSER gunicorn\n\nEXPOSE 8000\n\nENTRYPOINT [\"docker-entrypoint.sh\"]\nCMD [\"--help\"]\n"
  },
  {
    "path": "docker/docker-entrypoint.sh",
    "content": "#!/bin/bash\nset -e\n\n# Allow running other commands (e.g., bash for debugging)\nif [ \"${1:0:1}\" = '-' ] || [ -z \"${1##*:*}\" ]; then\n    # First arg is a flag or contains ':' (app:callable), run gunicorn\n\n    # Build bind address from GUNICORN_HOST and GUNICORN_PORT, or use GUNICORN_BIND\n    PORT=\"${GUNICORN_PORT:-8000}\"\n    BIND=\"${GUNICORN_BIND:-${GUNICORN_HOST:-0.0.0.0}:${PORT}}\"\n\n    # Add bind if not specified in args or GUNICORN_ARGS\n    if [[ ! \" $* $GUNICORN_ARGS \" =~ \" --bind \" ]] && [[ ! \" $* $GUNICORN_ARGS \" =~ \" -b \" ]] && [[ ! \"$* $GUNICORN_ARGS\" =~ --bind= ]] && [[ ! \"$* $GUNICORN_ARGS\" =~ -b= ]]; then\n        set -- --bind \"$BIND\" \"$@\"\n    fi\n\n    # Add workers if not specified - default to (2 * CPU_COUNT) + 1\n    if [[ ! \" $* $GUNICORN_ARGS \" =~ \" --workers \" ]] && [[ ! \" $* $GUNICORN_ARGS \" =~ \" -w \" ]] && [[ ! \"$* $GUNICORN_ARGS\" =~ --workers= ]] && [[ ! \"$* $GUNICORN_ARGS\" =~ -w= ]]; then\n        WORKERS=\"${GUNICORN_WORKERS:-$(( 2 * $(nproc) + 1 ))}\"\n        set -- --workers \"$WORKERS\" \"$@\"\n    fi\n\n    # Append GUNICORN_ARGS if set\n    if [ -n \"$GUNICORN_ARGS\" ]; then\n        exec gunicorn $GUNICORN_ARGS \"$@\"\n    fi\n\n    exec gunicorn \"$@\"\nfi\n\n# Otherwise, run the command as-is (e.g., bash, sh, python)\nexec \"$@\"\n"
  },
  {
    "path": "docs/README.md",
    "content": "# Generate Documentation\n\n## Requirements\n\nInstall the documentation dependencies with:\n\n```bash\npip install -r requirements_dev.txt\n```\n\nThis provides MkDocs with the Material theme and supporting plugins.\n\n## Build static HTML\n\n```bash\nmkdocs build\n```\n\nThe rendered site is emitted into the `site/` directory.\n\n## Preview locally\n\n```bash\nmkdocs serve\n```\n\nThis serves the documentation at http://127.0.0.1:8000/ with live reload.\n"
  },
  {
    "path": "docs/content/2010-news.md",
    "content": "<span id=\"news-2010\"></span>\n# Changelog - 2010\n\n## 0.12.0 / 2010-12-22\n\n- Add support for logging configuration using a ini file.\n  It uses the standard Python logging's module Configuration\n  file format and allows anyone to use his custom file handler\n- Add IPV6 support\n- Add multidomain application example\n- Improve gunicorn_django command when importing settings module\n  using DJANGO_SETTINGS_MODULE environment variable\n- Send appropriate error status on http parsing\n- Fix pidfile, set permissions so other user can read\n  it and use it.\n- Fix temporary file leaking\n- Fix setpgrp issue, can now be launched via ubuntu upstart\n- Set the number of workers to zero on WINCH\n\n## 0.11.2 / 2010-10-30\n\n* Add SERVER_SOFTWARE to the os.environ\n* Add support for django settings environment variable\n* Add support for logging configuration in Paster ini-files\n* Improve arbiter notification in asynchronous workers\n* Display the right error when a worker can't be used\n* Fix Django support\n* Fix HUP with Paster applications\n* Fix readline in wsgi.input\n\n## 0.11.1 / 2010-09-02\n\n* Implement max-requests feature to prevent memory leaks.\n* Added 'worker_exit' server hook.\n* Reseed the random number generator after fork().\n* Improve Eventlet worker.\n* Fix Django command `run_gunicorn`.\n* Fix the default proc name internal setting.\n* Workaround to prevent Gevent worker to segfault on MacOSX.\n\n## 0.11.0 / 2010-08-12\n\n* Improve dramatically performances of Gevent and Eventlet workers\n* Optimize HTTP parsing\n* Drop Server and Date headers in start_response when provided.\n* Fix latency issue in async workers\n\n## 0.10.1 / 2010-08-06\n\n* Improve gevent's workers. Add \"egg:gunicorn#gevent_wsgi\" worker using\n  `gevent.wsgi <http://www.gevent.org/gevent.wsgi.html>`_ and\n  \"egg:gunicorn#gevent_pywsgi\" worker using `gevent.pywsgi\n  <http://www.gevent.org/gevent.pywsgi.html>`_ .\n  **\"egg:gunicorn#gevent\"** using our own HTTP parser is still here and\n  is **recommended** for normal uses. Use the \"gevent.wsgi\" parser if you\n  need really fast connections and don't need streaming, keepalive or ssl.\n* Add pre/post request hooks\n* Exit more quietly\n* Fix gevent dns issue\n\n## 0.10.0 / 2010-07-08\n\n* New HTTP parser.\n* New HUP behaviour. Re-reads the configuration and then reloads all\n  worker processes without changing the master process id. Helpful for\n  code reloading and monitoring applications like supervisord and runit.\n* Added a preload configuration parameter. By default, application code\n  is now loaded after a worker forks. This couple with the new HUP\n  handling can be used for dev servers to do hot code reloading. Using\n  the preload flag can help a bit in small memory VM's.\n* Allow people to pass command line arguments to WSGI applications. See:\n  `examples/alt_spec.py\n  <http://github.com/benoitc/gunicorn/raw/master/examples/alt_spec.py>`_\n* Added an example gevent reloader configuration:\n  `examples/example_gevent_reloader.py\n  <http://github.com/benoitc/gunicorn/blob/master/examples/example_gevent_reloader.py>`_.\n* New gevent worker \"egg:gunicorn#gevent2\", working with gevent.wsgi.\n* Internal refactoring and various bug fixes.\n* New documentation website.\n\n## 0.9.1 / 2010-05-26\n\n* Support https via X-Forwarded-Protocol or X-Forwarded-Ssl headers\n* Fix configuration\n* Remove -d options which was used instead of -D for daemon.\n* Fix umask in unix socket\n\n## 0.9.0 / 2010-05-24\n\n* Added *when_ready* hook. Called just after the server is started\n* Added *preload* setting. Load application code before the worker processes\n  are forked.\n* Refactored Config\n* Fix pidfile\n* Fix QUIT/HUP in async workers\n* Fix reexec\n* Documentation improvements\n\n## 0.8.1 / 2010-04-29\n\n* Fix builtins import in config\n* Fix installation with pip\n* Fix Tornado WSGI support\n* Delay application loading until after processing all configuration\n\n## 0.8.0 / 2010-04-22\n\n* Refactored Worker management for better async support. Now use the -k option\n  to set the type of request processing to use\n* Added support for Tornado_\n\n## 0.7.2 / 2010-04-15\n\n* Added --spew option to help debugging (installs a system trace hook)\n* Some fixes in async arbiters\n* Fix a bug in start_response on error\n\n## 0.7.1 / 2010-04-01\n\n* Fix bug when responses have no body.\n\n## 0.7.0 / 2010-03-26\n\n* Added support for Eventlet_ and Gevent_ based workers.\n* Added Websockets_ support\n* Fix Chunked Encoding\n* Fix SIGWINCH on OpenBSD_\n* Fix `PEP 333`_ compliance for the write callable.\n\n## 0.6.5 / 2010-03-11\n\n* Fix pidfile handling\n* Fix Exception Error\n\n## 0.6.4 / 2010-03-08\n\n* Use cStringIO for performance when possible.\n* Fix worker freeze when a remote connection closes unexpectedly.\n\n## 0.6.3 / 2010-03-07\n\n* Make HTTP parsing faster.\n* Various bug fixes\n\n## 0.6.2 / 2010-03-01\n\n* Added support for chunked response.\n* Added proc_name option to the config file.\n* Improved the HTTP parser. It now uses buffers instead of strings to store\n  temporary data.\n* Improved performance when sending responses.\n* Workers are now murdered by age (the oldest is killed first).\n\n## 0.6.1 / 2010-02-24\n\n* Added gunicorn config file support for Django admin command\n* Fix gunicorn config file. -c was broken.\n* Removed TTIN/TTOU from workers which blocked other signals.\n\n## 0.6.0 / 2010-02-22\n\n* Added setproctitle support\n* Change privilege switch behavior. We now work like NGINX, master keeps the\n  permissions, new uid/gid permissions are only set for workers.\n\n## 0.5.1 / 2010-02-22\n\n* Fix umask\n* Added Debian packaging\n\n## 0.5.0 / 2010-02-20\n\n* Added `configuration file <configuration.html>`_ handler.\n* Added support for pre/post fork hooks\n* Added support for before_exec hook\n* Added support for unix sockets\n* Added launch of workers processes under different user/group\n* Added umask option\n* Added SCRIPT_NAME support\n* Better support of some exotic settings for Django projects\n* Better support of Paste-compatible applications\n* Some refactoring to make the code easier to hack\n* Allow multiple keys in request and response headers\n\n.. _Tornado: http://www.tornadoweb.org/\n.. _`PEP 333`: https://www.python.org/dev/peps/pep-0333/\n.. _Eventlet: http://eventlet.net/\n.. _Gevent: http://www.gevent.org/\n.. _OpenBSD: https://www.openbsd.org/\n.. _Websockets: https://html.spec.whatwg.org/multipage/web-sockets.html\n"
  },
  {
    "path": "docs/content/2011-news.md",
    "content": "<span id=\"news-2011\"></span>\n# Changelog - 2011\n\n## 0.13.4 / 2011-09-23\n\n- fix util.closerange function used to prevent leaking fds on python 2.5\n  (typo.md)\n\n## 0.13.3 / 2011-09-19\n- refactor gevent worker\n- prevent leaking fds on reexec\n- fix inverted request_time computation\n\n## 0.13.2 / 2011-09-17\n\n- Add support for Tornado 2.0 in tornado worker\n- Improve access logs: allows customisation of the log format & add\n  request time\n- Logger module is now pluggable\n- Improve graceful shutdown in Python versions >= 2.6\n- Fix post_request root arity for compatibility\n- Fix sendfile support\n- Fix Django reloading\n\n## 0.13.1 / 2011-08-22\n\n- Fix unix socket. log argument was missing.\n\n## 0.13.0 / 2011-08-22\n\n- Improve logging: allows file-reopening and add access log file\n  compatible with the `apache combined log format <http://httpd.apache.org/docs/2.0/logs.html#combined>`_\n- Add the possibility to set custom SSL headers. X-Forwarded-Protocol\n  and X-Forwarded-SSL are still the default\n- New `on_reload` hook to customize how gunicorn spawn new workers on\n  SIGHUP\n- Handle projects with relative path in django_gunicorn command\n- Preserve path parameters in PATH_INFO\n- post_request hook now accepts the environ as argument.\n- When stopping the arbiter, close the listener asap.\n- Fix Django command `run_gunicorn` in settings reloading\n- Fix Tornado_ worker exiting\n- Fix the use of sendfile in wsgi.file_wrapper\n\n\n## 0.12.2 / 2011-05-18\n\n- Add wsgi.file_wrapper optimised for FreeBSD, Linux & MacOSX (use\n  sendfile if available)\n- Fix django run_gunicorn command. Make sure we reload the application\n  code.\n- Fix django localisation\n- Compatible with gevent 0.14dev\n\n## 0.12.1 / 2011-03-23\n\n- Add \"on_starting\" hook. This hook can be used to set anything before\n  the arbiter really start\n- Support bdist_rpm in setup\n- Improve content-length handling (pep 3333)\n- Improve Django support\n- Fix daemonizing (#142)\n- Fix ipv6 handling\n\n\n.. _Tornado: http://www.tornadoweb.org/\n"
  },
  {
    "path": "docs/content/2012-news.md",
    "content": "<span id=\"news-2012\"></span>\n# Changelog - 2012\n\n## 0.17.0 / 2012-12-25\n\n- allows gunicorn to bind to multiple address\n- add SSL support\n- add syslog support\n- add nworkers_changed hook\n- add response arg for post_request hook\n- parse command line with argparse (replace deprecated optparse)\n- fix PWD detection in arbiter\n- miscellaneous PEP8 fixes\n\n## 0.16.1 / 2012-11-19\n\n- Fix packaging\n\n## 0.16.0 / 2012-11-19\n\n- **Added support for Python 3.2 & 3.3**\n- Expose --pythonpath command to all gunicorn commands\n- Honor $PORT environment variable, useful for deployment on heroku\n- Removed support for Python 2.5\n- Make sure we reopen the logs on the console\n- Fix django settings module detection from path\n- Reverted timeout for client socket. Fix issue on blocking issues.\n- Fixed gevent worker\n\n## 0.15.0 / 2012-10-18\n\n- new documentation site on https://gunicorn.org\n- new website on http://gunicorn.org\n- add `haproxy PROXY protocol <http://haproxy.1wt.eu/download/1.5/doc/proxy-protocol.txt>`_ support\n- add  ForwardedAllowIPS option: allows to filter Front-end's IPs\n  allowed to handle X-Forwarded-* headers.\n- add callable hooks for paster config\n- add x-forwarded-proto as secure scheme default (Heroku is using this)\n- allows gunicorn to load a pre-compiled application\n- support file reopening & reexec for all loggers\n- initialize the logging config file with defaults.\n- set timeout for client socket (slow client DoS).\n- NoMoreData, ChunkMissingTerminator, InvalidChunkSize are now\n  IOError exceptions\n- fix graceful shutdown in gevent\n- fix limit request line check\n\n## 0.14.6 / 2012-07-26\n\n\n- fix gevent & subproces\n- fix request line length check\n- fix keepalive = 0\n- fix tornado worker\n\n## 0.14.5 / 2012-06-24\n\n- fix logging during daemonisation\n\n## 0.14.4 / 2012-06-24\n\n- new --graceful-timeout option\n- fix multiple issues with request limit\n- more fixes in django settings resolutions\n- fix gevent.core import\n- fix keepalive=0 in eventlet worker\n- fix handle_error display with the unix worker\n- fix tornado.wsgi.WSGIApplication calling error\n\n- **breaking change**: take the control on graceful reload back.\n  graceful can't be overridden anymore using the on_reload function.\n\n## 0.14.3 / 2012-05-15\n\n- improvement: performance of http.body.Body.readline()\n- improvement: log HTTP errors in access log like Apache\n- improvement: display traceback when the worker fails to boot\n- improvement: makes gunicorn work with gevent 1.0\n- examples: websocket example now supports hybi13\n- fix: reopen log files after initialization\n- fix: websockets support\n- fix: django1.4 support\n- fix: only load the paster application 1 time\n\n## 0.14.2 / 2012-03-16\n\n- add validate_class validator: allows to use a class or a method to\n  initialize the app during in-code configuration\n- add support for max_requests in tornado worker\n- add support for disabling x_forwarded_for_header in tornado worker\n- gevent_wsgi is now an alias of gevent_pywsgi\n- Fix gevent_pywsgi worker\n\n## 0.14.1 / 2012-03-02\n\n- fixing source archive, reducing its size\n\n## 0.14.0 / 2012-02-27\n\n- check if Request line is too large: You can now pass the parameter\n  ``--limit-request-line`` or set the ``limit_request_line`` in your\n  configuration file to set the max size of the request line in bytes.\n- limit the number of headers fields and their size. Add\n  ``--limit-request-field`` and ``limit-request-field-size`` settings\n- add ``p`` variable to the log access format to log pidfile\n- add ``{HeaderName}o`` variable to the logo access format to log the\n  response header HeaderName\n- request header is now logged with the variable ``{HeaderName}i`` in the\n  access log file\n- improve error logging\n- support logging.configFile\n- support django 1.4 in both gunicorn_django & run_gunicorn command\n- improve reload in django run_gunicorn command (should just work now)\n- allows people to set the ``X-Forwarded-For`` header key and disable it by\n  setting an empty string.\n- fix support of Tornado\n- many other fixes.\n"
  },
  {
    "path": "docs/content/2013-news.md",
    "content": "<span id=\"news-2013\"></span>\n# Changelog - 2013\n\n## 18.0 / 2013-08-26\n\n- new: add ``-e/--env`` command line argument to pass an environment variables to\n  gunicorn\n- new: add ``--chdir`` command line argument to specified directory\n  before apps loading.  - new: add wsgi.file_wrapper support in async workers\n- new: add ``--paste`` command line argument to set the paster config file\n- deprecated: the command ``gunicorn_django`` is now deprecated. You should now\n  run your application with the WSGI interface installed with your project (see\n  https://docs.djangoproject.com/en/1.4/howto/deployment/wsgi/gunicorn/) for\n  more infos.\n- deprecated:  the command ``gunicorn_paste`` is deprecated. You now should use\n  the new ``--paste`` argument to set the configuration file of your paster\n  application.\n- fix: Removes unmatched leading quote from the beginning of the default access\n  log format string\n- fix: null timeout\n- fix: gevent worker\n- fix: don't reload the paster app when using pserve\n- fix: after closing for error do not keep alive the connection\n- fix: responses 1xx, 204 and 304 should not force the connection to be closed\n\n## 17.5 / 2013-07-03\n\n- new: add signals documentation\n- new: add post_worker_init hook for workers\n- new: try to use gunicorn.conf.py in current folder as the default\n  config file.\n- fix graceful timeout with the Eventlet worker\n- fix: don't raise an error when closing the socket if already closed\n- fix: fix --settings parameter for django application and try to find\n  the django settings when using the ``gunicorn`` command.\n- fix: give the initial global_conf to paster application\n- fix: fix 'Expect: 100-continue' support on Python 3\n\n### New versioning:\n\nWith this release, the versioning of Gunicorn is changing. Gunicorn is\nstable since a long time and there is no point to release a \"1.0\" now.\nIt should have been done since a long time. 0.17 really meant it was the\n17th stable version. From the beginning we have only 2 kind of\nreleases:\n\nmajor release: releases with major changes or huge features added\nservices releases: fixes and minor features added So from now we will\napply the following versioning ``<major>.<service>``. For example ``17.5`` is a\nservice release.\n\n## 0.17.4 / 2013-04-24\n\n- fix unix socket address parsing\n\n## 0.17.3 / 2013-04-23\n\n- add systemd sockets support\n- add ``python -m gunicorn.app.wsgiapp`` support\n- improve logger class inheritance\n- exit when the config file isn't found\n- add the -R option to enable stdio inheritance in daemon mode\n- don't close file descriptors > 3 in daemon mode\n- improve STDOUT/STDERR logging\n- fix pythonpath option\n- fix pidfile creation on Python 3\n- fix gevent worker exit\n- fix ipv6 detection when the platform isn't supporting it\n\n## 0.17.2 / 2013-01-07\n\n- optimize readline\n- make imports errors more visible when loading an app or a logging\n  class\n- fix tornado worker: don't pass ssl options if there are none\n- fix PEP3333: accept only bytetrings in the response body\n- fix support on CYGWIN platforms\n\n## 0.17.1 / 2013-01-05\n\n- add syslog facility name setting\n- fix ``--version`` command line argument\n- fix wsgi url_scheme for https\n"
  },
  {
    "path": "docs/content/2014-news.md",
    "content": "<span id=\"news-2014\"></span>\n# Changelog - 2014\n\n!!! note\n    Please see [news](news.md) for the latest changes.\n\n\n## 19.1.1 / 2014-08-16\n\n### Changes\n\n### Core\n\n- fix [Issue #835](https://github.com/benoitc/gunicorn/issues/835): display correct pid of already running instance\n- fix [PR #833](https://github.com/benoitc/gunicorn/pull/833): fix `PyTest` class in setup.py.\n\n\n### Logging\n\n- fix [Issue #838](https://github.com/benoitc/gunicorn/issues/838): statsd logger, send statsd timing metrics in milliseconds\n- fix [Issue #839](https://github.com/benoitc/gunicorn/issues/839): statsd logger, allows for empty log message while pushing\n  metrics and restore worker number in DEBUG logs\n- fix [Issue #850](https://github.com/benoitc/gunicorn/issues/850): add timezone to logging\n- fix [Issue #853](https://github.com/benoitc/gunicorn/issues/853): Respect logger_class setting unless statsd is on\n\n### AioHttp worker\n\n- fix [Issue #830](https://github.com/benoitc/gunicorn/issues/830) make sure gaiohttp worker is shipped with gunicorn.\n\n## 19.1 / 2014-07-26\n\n### Changes\n\n### Core\n\n- fix [Issue #785](https://github.com/benoitc/gunicorn/issues/785): handle binary type address given to a client socket address\n- fix graceful shutdown. make sure QUIT and TERMS signals are switched everywhere.\n- [Issue #799](https://github.com/benoitc/gunicorn/issues/799): fix support loading config from module\n- [Issue #805](https://github.com/benoitc/gunicorn/issues/805): fix check for file-like objects\n- fix [Issue #815](https://github.com/benoitc/gunicorn/issues/815): args validation in WSGIApplication.init\n- fix [Issue #787](https://github.com/benoitc/gunicorn/issues/787): check if we load a pyc file or not.\n\n\n### Tornado worker\n\n- fix [Issue #771](https://github.com/benoitc/gunicorn/issues/771): support tornado 4.0\n- fix [Issue #783](https://github.com/benoitc/gunicorn/issues/783): x_headers error. The x-forwarded-headers option has been removed\n  in `c4873681299212d6082cd9902740eef18c2f14f1\n  <https://github.com/benoitc/gunicorn/commit/c4873681299212d6082cd9902740eef18c2f14f1>`_.\n  The discussion is available on [PR #633](https://github.com/benoitc/gunicorn/pull/633).\n\n\n### AioHttp worker\n\n- fix: fetch all body in input. fix [Issue #803](https://github.com/benoitc/gunicorn/issues/803)\n- fix: don't install the worker if python < 3.3\n- fix [Issue #822](https://github.com/benoitc/gunicorn/issues/822): Support UNIX sockets in gaiohttp worker\n\n\n### Async worker\n\n- fix [Issue #790](https://github.com/benoitc/gunicorn/issues/790): StopIteration shouldn't be caught at this level.\n\n\n### Logging\n\n- add statsd logging handler fix [Issue #748](https://github.com/benoitc/gunicorn/issues/748)\n\n\n### Paster\n\n- fix [Issue #809](https://github.com/benoitc/gunicorn/issues/809): Set global logging configuration from a Paste config.\n\n\n### Extra\n\n- fix RuntimeError in gunicorn.reloader ([Issue #807](https://github.com/benoitc/gunicorn/issues/807))\n\n\n### Documentation\n\n- update faq: put a note on how `watch logs in the console\n  <https://gunicorn.org/faq/#why-i-dont-see-any-logs-in-the-console>`_\n  since many people asked for it.\n\n\n## 19.0 / 2014-06-12\n\nGunicorn 19.0 is a major release with new features and fixes. This\nversion improve a lot the usage of Gunicorn with python 3 by adding `two\nnew workers <https://gunicorn.org/design/#asyncio-workers>`_\nto it: `gthread` a fully threaded async worker using futures and `gaiohttp` a\nworker using asyncio.\n\n\n### Breaking Changes\n\n### Switch QUIT and TERM signals\n\nWith this change, when gunicorn receives a QUIT all the workers are\nkilled immediately and exit and TERM is used for the graceful shutdown.\n\nNote: the old behaviour was based on the NGINX but the new one is more\ncorrect according the following doc:\n\nhttps://www.gnu.org/software/libc/manual/html_node/Termination-Signals.html\n\nalso it is complying with the way the signals are sent by heroku:\n\nhttps://devcenter.heroku.com/articles/python-faq#what-constraints-exist-when-developing-applications-on-heroku\n\n### Deprecations\n\n`run_gunicorn`, `gunicorn_django` and `gunicorn_paster` are now\ncompletely deprecated and will be removed in the next release. Use the\n`gunicorn` command instead.\n\n\n### Changes\n\n### core\n\n- add aiohttp worker named `gaiohttp` using asyncio. Full async worker\n  on python 3.\n- fix HTTP-violating excess whitespace in write_error output\n- fix: try to log what happened in the worker after a timeout, add a\n  `worker_abort` hook on SIGABRT signal.\n- fix: save listener socket name in workers so we can handle buffered\n  keep-alive requests after the listener has closed.\n- add on_exit hook called just before exiting gunicorn.\n- add support for python 3.4\n- fix: do not swallow unexpected errors when reaping\n- fix: remove incompatible SSL option with python 2.6\n- add new async gthread worker and `--threads` options allows to set multiple\n  threads to listen on connection\n- deprecate `gunicorn_django` and `gunicorn_paster`\n- switch QUIT and TERM signal\n- reap workers in SIGCHLD handler\n- add universal wheel support\n- use `email.utils.formatdate` in gunicorn.util.http_date\n- deprecate the `--debug` option\n- fix: log exceptions that occur after response start …\n- allows loading of applications from `.pyc` files (#693)\n- fix: issue #691, raw_env config file parsing\n- use a dynamic timeout to wait for the optimal time. (Reduce power\n  usage)\n- fix python3 support when notifying the arbiter\n- add: honor $WEB_CONCURRENCY environment variable. Useful for heroku\n  setups.\n- add: include tz offset in access log\n- add: include access logs in the syslog handler.\n- add --reload option for code reloading\n- add the capability to load `gunicorn.base.Application` without the loading of\n  the arguments of the command line. It allows you to [embed gunicorn in   your own application](custom.md).\n- improve: set wsgi.multithread to True for async workers\n- fix logging: make sure to redirect wsgi.errors when needed\n- add: syslog logging can now be done to a unix socket\n- fix logging: don't try to redirect stdout/stderr to the logfile.\n- fix logging: don't propagate log\n- improve logging: file option can be overridden by the gunicorn options\n  `--error-logfile` and `--access-logfile` if they are given.\n- fix: don't override SERVER_* by the Host header\n- fix: handle_error\n- add more option to configure SSL\n- fix: sendfile with SSL\n- add: worker_int callback (to react on SIGTERM)\n- fix: don't depend on entry point for internal classes, now absolute\n  modules path can be given.\n- fix: Error messages are now encoded in latin1\n- fix: request line length check\n- improvement: proxy_allow_ips: Allow proxy protocol if \"*\" specified\n- fix: run worker's `setup` method  before setting num_workers\n- fix: FileWrapper inherit from `object` now\n- fix: Error messages are now encoded in latin1\n- fix: don't spam the console on SIGWINCH.\n- fix: logging -don't stringify T and D logging atoms (#621)\n- add support for the latest django version\n- deprecate `run_gunicorn` django option\n- fix: sys imported twice\n\n\n### gevent worker\n\n- fix: make sure to stop all listeners\n- fix: monkey patching is now done in the worker\n- fix: \"global name 'hub' is not defined\"\n- fix: reinit `hub` on old versions of gevent\n- support gevent 1.0\n- fix: add subprocess in monkey patching\n- fix: add support for multiple listener\n\n\n### eventlet worker\n\n- fix: merge duplicate EventletWorker.init_process method (fixes #657)\n- fix: missing errno import for eventlet sendfile patch\n- fix: add support for multiple listener\n\n\n### tornado worker\n\n- add graceful stop support\n"
  },
  {
    "path": "docs/content/2015-news.md",
    "content": "<span id=\"news-2015\"></span>\n# Changelog - 2015\n\n!!! note\n    Please see [news](news.md) for the latest changes.\n\n\n## 19.4.3 / 2015/12/30\n\n- fix: don't check if a file is writable using os.stat with SELINUX ([Issue #1171](https://github.com/benoitc/gunicorn/issues/1171))\n\n## 19.4.2 / 2015/12/29\n\n### Core\n\n- improvement: handle HaltServer in manage_workers ([Issue #1095](https://github.com/benoitc/gunicorn/issues/1095))\n- fix: Do not rely on sendfile sending requested count ([Issue #1155](https://github.com/benoitc/gunicorn/issues/1155))\n- fix: claridy --no-sendfile default ([Issue #1156](https://github.com/benoitc/gunicorn/issues/1156))\n- fix: LoggingCatch sendfile failure from no file descriptor ([Issue #1160](https://github.com/benoitc/gunicorn/issues/1160))\n\n### Logging\n\n- fix: Always send access log to syslog if syslog is on\n- fix: check auth before trying to own a file ([Issue #1157](https://github.com/benoitc/gunicorn/issues/1157))\n\n\n### Documentation\n\n- fix: Fix Slowloris broken link. ([Issue #1142](https://github.com/benoitc/gunicorn/issues/1142))\n- Tweak markup in faq.rst\n\n### Testing\n\n- fix: gaiohttp test ([Issue #1164](https://github.com/benoitc/gunicorn/issues/1164))\n\n## 19.4.1 / 2015/11/25\n\n- fix tornado worker ([Issue #1154](https://github.com/benoitc/gunicorn/issues/1154))\n\n## 19.4.0 / 2015/11/20\n\n### Core\n\n- fix: make sure that a user is able to access to the logs after dropping a\n  privilege ([Issue #1116](https://github.com/benoitc/gunicorn/issues/1116))\n- improvement: inherit the `Exception` class where it needs to be ([Issue #997](https://github.com/benoitc/gunicorn/issues/997))\n- fix: make sure headers are always encoded as latin1 RFC 2616 ([Issue #1102](https://github.com/benoitc/gunicorn/issues/1102))\n- improvement: reduce arbiter noise ([Issue #1078](https://github.com/benoitc/gunicorn/issues/1078))\n- fix: don't close the unix socket when the worker exit ([Issue #1088](https://github.com/benoitc/gunicorn/issues/1088))\n- improvement: Make last logged worker count an explicit instance var ([Issue #1078](https://github.com/benoitc/gunicorn/issues/1078))\n- improvement: prefix config file with its type ([Issue #836](https://github.com/benoitc/gunicorn/issues/836))\n- improvement: pidfile handing ([Issue #1042](https://github.com/benoitc/gunicorn/issues/1042))\n- fix: catch OSError as well as ValueError on race condition ([Issue #1052](https://github.com/benoitc/gunicorn/issues/1052))\n- improve support of ipv6 by backporting urlparse.urlsplit from Python 2.7 to\n  Python 2.6.\n- fix: raise InvalidRequestLine when the line contains malicious data\n  ([Issue #1023](https://github.com/benoitc/gunicorn/issues/1023))\n- fix: fix argument to disable sendfile\n- fix: add gthread to the list of supported workers ([Issue #1011](https://github.com/benoitc/gunicorn/issues/1011))\n- improvement: retry socket binding up to five times upon EADDRNOTAVAIL\n  ([Issue #1004](https://github.com/benoitc/gunicorn/issues/1004))\n- **breaking change**: only honor headers that can be encoded in ascii to comply to\n  the RFC 7230 (See [Issue #1151](https://github.com/benoitc/gunicorn/issues/1151)).\n\n### Logging\n\n- add new parameters to access log ([Issue #1132](https://github.com/benoitc/gunicorn/issues/1132))\n- fix: make sure that files handles are correctly reopened on HUP\n  ([Issue #627](https://github.com/benoitc/gunicorn/issues/627))\n- include request URL in error message ([Issue #1071](https://github.com/benoitc/gunicorn/issues/1071))\n- get username in access logs ([Issue #1069](https://github.com/benoitc/gunicorn/issues/1069))\n- fix statsd logging support on Python 3 ([Issue #1010](https://github.com/benoitc/gunicorn/issues/1010))\n\n### Testing\n\n- use last version of mock.\n- many fixes in Travis CI support\n- miscellaneous improvements in tests\n\n### Thread worker\n\n- fix: Fix self.nr usage in ThreadedWorker so that auto restart works as\n  expected ([Issue #1031](https://github.com/benoitc/gunicorn/issues/1031))\n\n### Gevent worker\n\n- fix quit signal handling ([Issue #1128](https://github.com/benoitc/gunicorn/issues/1128))\n- add support for Python 3 ([Issue #1066](https://github.com/benoitc/gunicorn/issues/1066))\n- fix: make graceful shutdown thread-safe ([Issue #1032](https://github.com/benoitc/gunicorn/issues/1032))\n\n### Tornado worker\n\n- fix ssl options ([Issue #1146](https://github.com/benoitc/gunicorn/issues/1146), [Issue #1135](https://github.com/benoitc/gunicorn/issues/1135))\n- don't check timeout when stopping gracefully ([Issue #1106](https://github.com/benoitc/gunicorn/issues/1106))\n\n### AIOHttp worker\n\n- add SSL support ([Issue #1105](https://github.com/benoitc/gunicorn/issues/1105))\n\n### Documentation\n\n- fix link to proc name setting ([Issue #1144](https://github.com/benoitc/gunicorn/issues/1144))\n- fix worker class documentation ([Issue #1141](https://github.com/benoitc/gunicorn/issues/1141), [Issue #1104](https://github.com/benoitc/gunicorn/issues/1104))\n- clarify graceful timeout documentation ([Issue #1137](https://github.com/benoitc/gunicorn/issues/1137))\n- don't duplicate NGINX config files examples ([Issue #1050](https://github.com/benoitc/gunicorn/issues/1050), [Issue #1048](https://github.com/benoitc/gunicorn/issues/1048))\n- add `web.py` framework example ([Issue #1117](https://github.com/benoitc/gunicorn/issues/1117))\n- update Debian/Ubuntu installations instructions ([Issue #1112](https://github.com/benoitc/gunicorn/issues/1112))\n- clarify `pythonpath` setting description ([Issue #1080](https://github.com/benoitc/gunicorn/issues/1080))\n- tweak some example for python3\n- clarify `sendfile` documentation\n- miscellaneous typos in source code comments (thanks!)\n- clarify why REMOTE_ADD may not be the user's IP address ([Issue #1037](https://github.com/benoitc/gunicorn/issues/1037))\n\n\n### Misc\n\n- fix: reloader should survive SyntaxError ([Issue #994](https://github.com/benoitc/gunicorn/issues/994))\n- fix: expose the reloader class to the worker.\n\n\n\n## 19.3.0 / 2015/03/06\n\n### Core\n\n- fix: [Issue #978](https://github.com/benoitc/gunicorn/issues/978) make sure a listener is inheritable\n- add `check_config` class method to workers\n- fix: [Issue #983](https://github.com/benoitc/gunicorn/issues/983) fix select timeout in sync worker with multiple\n  connections\n- allows workers to access to the reloader. close [Issue #984](https://github.com/benoitc/gunicorn/issues/984)\n- raise TypeError instead of AssertionError\n\n### Logging\n\n- make Logger.loglevel a class attribute\n\n### Documentation\n\n- fix: [Issue #988](https://github.com/benoitc/gunicorn/issues/988) fix syntax errors in examples/gunicorn_rc\n\n\n## 19.2.1 / 2015/02/4\n\n### Logging\n\n- expose loglevel in the Logger class\n\n### AsyncIO worker (gaiohttp.md)\n\n- fix [Issue #977](https://github.com/benoitc/gunicorn/issues/977) fix initial crash\n\n### Documentation\n\n- document security mailing-list in the contributing page.\n\n## 19.2 / 2015/01/30\n\n### Core\n\n- optimize the sync workers when listening on a single interface\n- add `--sendfile` settings to enable/disable sendfile. fix [Issue #856](https://github.com/benoitc/gunicorn/issues/856) .\n- add the selectors module to the code base. [Issue #886](https://github.com/benoitc/gunicorn/issues/886)\n- add `--max-requests-jitter` setting to set the maximum jitter to add to the\n  max-requests setting.\n- fix [Issue #899](https://github.com/benoitc/gunicorn/issues/899) propagate proxy_protocol_info to keep-alive requests\n- fix [Issue #863](https://github.com/benoitc/gunicorn/issues/863) worker timeout: dynamic timeout has been removed\n- fix: Avoid world writable file\n\n### Logging\n\n- fix [Issue #941](https://github.com/benoitc/gunicorn/issues/941)  set logconfig default to paster more trivially\n- add statsd-prefix config setting: set the prefix to use when emitting statsd\n  metrics\n- [Issue #832](https://github.com/benoitc/gunicorn/issues/832) log to console by default\n\n### Thread Worker\n\n- fix [Issue #908](https://github.com/benoitc/gunicorn/issues/908) make sure the worker can continue to accept requests\n\n### Eventlet Worker\n\n- fix [Issue #867](https://github.com/benoitc/gunicorn/issues/867) Fix eventlet shutdown to actively shut down the workers.\n\n### Documentation\n\nMany improvements and fixes have been done, see the detailed changelog for\nmore information.\n"
  },
  {
    "path": "docs/content/2016-news.md",
    "content": "<span id=\"news-2016\"></span>\n# Changelog - 2016\n\n!!! note\n    Please see [news](news.md) for the latest changes\n\n\n## 19.6.0 / 2016/05/21\n\n### Core & Logging\n\n- improvement of the binary upgrade behaviour using USR2: remove file locking ([Issue #1270](https://github.com/benoitc/gunicorn/issues/1270))\n- add the ``--capture-output`` setting to capture stdout/stderr tot the log\n  file ([Issue #1271](https://github.com/benoitc/gunicorn/issues/1271))\n- Allow disabling ``sendfile()`` via the ``SENDFILE`` environment variable\n  ([Issue #1252](https://github.com/benoitc/gunicorn/issues/1252))\n- fix reload under pycharm ([Issue #1129](https://github.com/benoitc/gunicorn/issues/1129))\n\n### Workers\n\n- fix: make sure to remove the signal from the worker pipe ([Issue #1269](https://github.com/benoitc/gunicorn/issues/1269))\n- fix: **gthread** worker, handle removed socket in the select loop\n  ([Issue #1258](https://github.com/benoitc/gunicorn/issues/1258))\n\n## 19.5.0 / 2016/05/10\n\n### Core\n\n- fix: Ensure response to HEAD request won't have message body\n- fix: lock domain socket and remove on last arbiter exit ([Issue #1220](https://github.com/benoitc/gunicorn/issues/1220))\n- improvement: use EnvironmentError instead of socket.error ([Issue #939](https://github.com/benoitc/gunicorn/issues/939))\n- add: new ``FORWARDED_ALLOW_IPS`` environment variable ([Issue #1205](https://github.com/benoitc/gunicorn/issues/1205))\n- fix: infinite recursion when destroying sockets ([Issue #1219](https://github.com/benoitc/gunicorn/issues/1219))\n- fix: close sockets on shutdown ([Issue #922](https://github.com/benoitc/gunicorn/issues/922))\n- fix: clean up sys.exc_info calls to drop circular refs ([Issue #1228](https://github.com/benoitc/gunicorn/issues/1228))\n- fix: do post_worker_init after load_wsgi ([Issue #1248](https://github.com/benoitc/gunicorn/issues/1248))\n\n### Workers\n\n- fix access logging in gaiohttp worker ([Issue #1193](https://github.com/benoitc/gunicorn/issues/1193))\n- eventlet: handle QUIT in a new coroutine ([Issue #1217](https://github.com/benoitc/gunicorn/issues/1217))\n- gevent: remove obsolete exception clauses in run ([Issue #1218](https://github.com/benoitc/gunicorn/issues/1218))\n- tornado: fix extra \"Server\" response header ([Issue #1246](https://github.com/benoitc/gunicorn/issues/1246))\n- fix: unblock the wait loop under python 3.5 in sync worker ([Issue #1256](https://github.com/benoitc/gunicorn/issues/1256))\n\n### Logging\n\n- fix: log message for listener reloading ([Issue #1181](https://github.com/benoitc/gunicorn/issues/1181))\n- Let logging module handle traceback printing ([Issue #1201](https://github.com/benoitc/gunicorn/issues/1201))\n- improvement: Allow configuring logger_class with statsd_host ([Issue #1188](https://github.com/benoitc/gunicorn/issues/1188))\n- fix: traceback formatting ([Issue #1235](https://github.com/benoitc/gunicorn/issues/1235))\n- fix: print error logs on stderr and access logs on stdout ([Issue #1184](https://github.com/benoitc/gunicorn/issues/1184))\n\n\n### Documentation\n\n- Simplify installation instructions in gunicorn.org ([Issue #1072](https://github.com/benoitc/gunicorn/issues/1072))\n- Fix URL and default worker type in example_config ([Issue #1209](https://github.com/benoitc/gunicorn/issues/1209))\n- update django doc url to 1.8 lts ([Issue #1213](https://github.com/benoitc/gunicorn/issues/1213))\n- fix: miscellaneous wording corrections ([Issue #1216](https://github.com/benoitc/gunicorn/issues/1216))\n- Add PSF License Agreement of selectors.py to NOTICE (:issue: `1226`)\n- document LOGGING overriding ([Issue #1051](https://github.com/benoitc/gunicorn/issues/1051))\n- put a note that error logs are only errors from Gunicorn ([Issue #1124](https://github.com/benoitc/gunicorn/issues/1124))\n- add a note about the requirements of the threads workers under python 2.x ([Issue #1200](https://github.com/benoitc/gunicorn/issues/1200))\n- add access_log_format to config example ([Issue #1251](https://github.com/benoitc/gunicorn/issues/1251))\n\n### Tests\n\n- Use more pytest.raises() in test_http.py\n\n\n## 19.4.5 / 2016/01/05\n\n- fix: NameError fileno in gunicorn.http.wsgi ([Issue #1178](https://github.com/benoitc/gunicorn/issues/1178))\n\n## 19.4.4 / 2016/01/04\n\n- fix: check if a fileobject can be used with sendfile(2.md) ([Issue #1174](https://github.com/benoitc/gunicorn/issues/1174))\n- doc: be more descriptive in errorlog option ([Issue #1173](https://github.com/benoitc/gunicorn/issues/1173))\n"
  },
  {
    "path": "docs/content/2017-news.md",
    "content": "<span id=\"news-2017\"></span>\n# Changelog - 2017\n\n!!! note\n    Please see [news](news.md) for the latest changes\n\n\n## 19.7.1 / 2017/03/21\n\n- fix: continue if SO_REUSEPORT seems to be available but fails ([Issue #1480](https://github.com/benoitc/gunicorn/issues/1480))\n- fix: support non-decimal values for the umask command line option ([Issue #1325](https://github.com/benoitc/gunicorn/issues/1325))\n\n## 19.7.0 / 2017/03/01\n\n- The previously deprecated ``gunicorn_django`` command has been removed.\n  Use the [gunicorn-cmd](run.md#gunicorn) command-line interface instead.\n- The previously deprecated ``django_settings`` setting has been removed.\n  Use the [raw-env](reference/settings.md#raw_env) setting instead.\n- The default value of [ssl-version](reference/settings.md#ssl_version) has been changed from\n  ``ssl.PROTOCOL_TLSv1`` to ``ssl.PROTOCOL_SSLv23``.\n- fix: initialize the group access list when initgroups is set ([Issue #1297](https://github.com/benoitc/gunicorn/issues/1297))\n- add environment variables to gunicorn access log format ([Issue #1291](https://github.com/benoitc/gunicorn/issues/1291))\n- add --paste-global-conf option ([Issue #1304](https://github.com/benoitc/gunicorn/issues/1304))\n- fix: print access logs to STDOUT ([Issue #1184](https://github.com/benoitc/gunicorn/issues/1184))\n- remove upper limit on max header size config ([Issue #1313](https://github.com/benoitc/gunicorn/issues/1313))\n- fix: print original exception on AppImportError ([Issue #1334](https://github.com/benoitc/gunicorn/issues/1334))\n- use SO_REUSEPORT if available ([Issue #1344](https://github.com/benoitc/gunicorn/issues/1344))\n- `fix leak <https://github.com/benoitc/gunicorn/commit/b4c41481e2d5ef127199a4601417a6819053c3fd>`_ of duplicate file descriptor for bound sockets.\n- add --reload-engine option, support inotify and other backends ([Issue #1368](https://github.com/benoitc/gunicorn/issues/1368), [Issue #1459](https://github.com/benoitc/gunicorn/issues/1459))\n- fix: reject request with invalid HTTP versions\n- add ``child_exit`` callback ([Issue #1394](https://github.com/benoitc/gunicorn/issues/1394))\n- add support for eventlets _AlreadyHandled object ([Issue #1406](https://github.com/benoitc/gunicorn/issues/1406))\n- format boot tracebacks properly with reloader ([Issue #1408](https://github.com/benoitc/gunicorn/issues/1408))\n- refactor socket activation and fd inheritance for better support of SystemD ([Issue #1310](https://github.com/benoitc/gunicorn/issues/1310))\n- fix: o fds are given by default in gunicorn ([Issue #1423](https://github.com/benoitc/gunicorn/issues/1423))\n- add ability to pass settings to GUNICORN_CMD_ARGS environment variable which helps in container world ([Issue #1385](https://github.com/benoitc/gunicorn/issues/1385))\n- fix:  catch access denied to pid file ([Issue #1091](https://github.com/benoitc/gunicorn/issues/1091))\n-  many additions and improvements to the documentation\n\n### Breaking Change\n\n- **Python 2.6.0** is the last supported version\n"
  },
  {
    "path": "docs/content/2018-news.md",
    "content": "<span id=\"news-2018\"></span>\n# Changelog - 2018\n\n!!! note\n    Please see [news](news.md) for the latest changes\n\n\n## 19.9.0 / 2018/07/03\n\n- fix: address a regression that prevented syslog support from working\n  ([Issue #1668](https://github.com/benoitc/gunicorn/issues/1668), [PR #1773](https://github.com/benoitc/gunicorn/pull/1773))\n- fix: correctly set `REMOTE_ADDR` on versions of Python 3 affected by\n  `Python Issue 30205 <https://bugs.python.org/issue30205>`_\n  ([Issue #1755](https://github.com/benoitc/gunicorn/issues/1755), [PR #1796](https://github.com/benoitc/gunicorn/pull/1796))\n- fix: show zero response length correctly in access log ([PR #1787](https://github.com/benoitc/gunicorn/pull/1787))\n- fix: prevent raising `AttributeError` when ``--reload`` is not passed\n  in case of a `SyntaxError` raised from the WSGI application.\n  ([Issue #1805](https://github.com/benoitc/gunicorn/issues/1805), [PR #1806](https://github.com/benoitc/gunicorn/pull/1806))\n- The internal module ``gunicorn.workers.async`` was renamed to ``gunicorn.workers.base_async``\n  since ``async`` is now a reserved word in Python 3.7.\n  ([PR #1527](https://github.com/benoitc/gunicorn/pull/1527))\n\n## 19.8.1 / 2018/04/30\n\n- fix: secure scheme headers when bound to a unix socket\n  ([Issue #1766](https://github.com/benoitc/gunicorn/issues/1766), [PR #1767](https://github.com/benoitc/gunicorn/pull/1767))\n\n## 19.8.0 / 2018/04/28\n\n- Eventlet 0.21.0 support ([Issue #1584](https://github.com/benoitc/gunicorn/issues/1584))\n- Tornado 5 support ([Issue #1728](https://github.com/benoitc/gunicorn/issues/1728), [PR #1752](https://github.com/benoitc/gunicorn/pull/1752))\n- support watching additional files with ``--reload-extra-file``\n  ([PR #1527](https://github.com/benoitc/gunicorn/pull/1527))\n- support configuring logging with a dictionary with ``--logging-config-dict``\n  ([Issue #1087](https://github.com/benoitc/gunicorn/issues/1087), [PR #1110](https://github.com/benoitc/gunicorn/pull/1110), [PR #1602](https://github.com/benoitc/gunicorn/pull/1602))\n- add support for the ``--config`` flag in the ``GUNICORN_CMD_ARGS`` environment\n  variable ([Issue #1576](https://github.com/benoitc/gunicorn/issues/1576), [PR #1581](https://github.com/benoitc/gunicorn/pull/1581))\n- disable ``SO_REUSEPORT`` by default and add the ``--reuse-port`` setting\n  ([Issue #1553](https://github.com/benoitc/gunicorn/issues/1553), [Issue #1603](https://github.com/benoitc/gunicorn/issues/1603), [PR #1669](https://github.com/benoitc/gunicorn/pull/1669))\n- fix: installing `inotify` on MacOS no longer breaks the reloader\n  ([Issue #1540](https://github.com/benoitc/gunicorn/issues/1540), [PR #1541](https://github.com/benoitc/gunicorn/pull/1541))\n- fix: do not throw ``TypeError`` when ``SO_REUSEPORT`` is not available\n  ([Issue #1501](https://github.com/benoitc/gunicorn/issues/1501), [PR #1491](https://github.com/benoitc/gunicorn/pull/1491))\n- fix: properly decode HTTP paths containing certain non-ASCII characters\n  ([Issue #1577](https://github.com/benoitc/gunicorn/issues/1577), [PR #1578](https://github.com/benoitc/gunicorn/pull/1578))\n- fix: remove whitespace when logging header values under gevent ([PR #1607](https://github.com/benoitc/gunicorn/pull/1607))\n- fix: close unlinked temporary files ([Issue #1327](https://github.com/benoitc/gunicorn/issues/1327), [PR #1428](https://github.com/benoitc/gunicorn/pull/1428))\n- fix: parse ``--umask=0`` correctly ([Issue #1622](https://github.com/benoitc/gunicorn/issues/1622), [PR #1632](https://github.com/benoitc/gunicorn/pull/1632))\n- fix: allow loading applications using relative file paths\n  ([Issue #1349](https://github.com/benoitc/gunicorn/issues/1349), [PR #1481](https://github.com/benoitc/gunicorn/pull/1481))\n- fix: force blocking mode on the gevent sockets ([Issue #880](https://github.com/benoitc/gunicorn/issues/880), [PR #1616](https://github.com/benoitc/gunicorn/pull/1616))\n- fix: preserve leading `/` in request path ([Issue #1512](https://github.com/benoitc/gunicorn/issues/1512), [PR #1511](https://github.com/benoitc/gunicorn/pull/1511))\n- fix: forbid contradictory secure scheme headers\n- fix: handle malformed basic authentication headers in access log\n  ([Issue #1683](https://github.com/benoitc/gunicorn/issues/1683), [PR #1684](https://github.com/benoitc/gunicorn/pull/1684))\n- fix: defer handling of ``USR1`` signal to a new greenlet under gevent\n  ([Issue #1645](https://github.com/benoitc/gunicorn/issues/1645), [PR #1651](https://github.com/benoitc/gunicorn/pull/1651))\n- fix: the threaded worker would sometimes close the wrong keep-alive\n  connection under Python 2 ([Issue #1698](https://github.com/benoitc/gunicorn/issues/1698), [PR #1699](https://github.com/benoitc/gunicorn/pull/1699))\n- fix: re-open log files on ``USR1`` signal using ``handler._open`` to\n  support subclasses of ``FileHandler`` ([Issue #1739](https://github.com/benoitc/gunicorn/issues/1739), [PR #1742](https://github.com/benoitc/gunicorn/pull/1742))\n- deprecation: the ``gaiohttp`` worker is deprecated, see the\n  [worker-class](reference/settings.md#worker_class) documentation for more information\n  ([Issue #1338](https://github.com/benoitc/gunicorn/issues/1338), [PR #1418](https://github.com/benoitc/gunicorn/pull/1418), [PR #1569](https://github.com/benoitc/gunicorn/pull/1569))\n"
  },
  {
    "path": "docs/content/2019-news.md",
    "content": "<span id=\"news-2019\"></span>\n# Changelog - 2019\n\n!!! note\n    Please see [news](news.md) for the latest changes\n\n\n## 20.0.4 / 2019/11/26\n\n- fix binding a socket using the file descriptor\n- remove support for the `bdist_rpm` build\n\n## 20.0.3 / 2019/11/24\n\n- fixed load of a config file without a Python extension\n- fixed `socketfromfd.fromfd` when defaults are not set\n\n!!! note\n    ```\n    ## 20.0.2 / 2019/11/23\n    \n    - fix changelog\n    \n    ## 20.0.1 / 2019/11/23\n    \n    - fixed the way the config module is loaded. `__file__` is now available\n    - fixed `wsgi.input_terminated`. It is always true.\n    - use the highest protocol version of openssl by default\n    - only support Python >= 3.5\n    - added `__repr__` method to `Config` instance\n    - fixed support of AIX platform and musl libc in  `socketfromfd.fromfd` function\n    - fixed support of applications loaded from a factory function\n    - fixed chunked encoding support to prevent any `request smuggling <https://portswigger.net/research/http-desync-attacks-request-smuggling-reborn>`_\n    - Capture os.sendfile before patching in gevent and eventlet workers.\n      fix `RecursionError`.\n    - removed locking in reloader when adding new files\n    - load the WSGI application before the loader to pick up all files\n\n{note}\nas documented in Flask and other places.\n```\n## 19.10.0 / 2019/11/23\n\n- unblock select loop during reload of a sync worker\n- security fix: http desync attack\n- handle `wsgi.input_terminated`\n- added support for str and bytes in unix  socket addresses\n- fixed `max_requests` setting\n- headers values are now encoded as LATN1, not ASCII\n- fixed `InotifyReloadeder`:  handle `module.__file__` is None\n- fixed compatibility with tornado 6\n- fixed root logging\n- Prevent removalof unix sockets from `reuse_port`\n- Clear tornado ioloop before os.fork\n- Miscellaneous fixes and improvement for linting using Pylint\n\n## 20.0 / 2019/10/30\n\n- Fixed `fdopen` `RuntimeWarning` in Python 3.8\n- Added  check and exception for str type on value in Response process_headers method.\n- Ensure WSGI header value is string before conducting regex search on it.\n- Added pypy3 to list of tested environments\n- Grouped `StopIteration` and `KeyboardInterrupt` exceptions with same body together in Arbiter.run()\n- Added `setproctitle` module to `extras_require` in setup.py\n- Avoid unnecessary chown of temporary files\n- Logging: Handle auth type case insensitively\n- Removed `util.import_module`\n- Removed fallback for `types.SimpleNamespace` in tests utils\n- Use `SourceFileLoader` instead instead of `execfile_`\n- Use `importlib` instead of `__import__` and eval`\n- Fixed eventlet patching\n- Added optional `datadog <https://www.datadoghq.com>`_ tags for statsd metrics\n- Header values now are encoded using latin-1, not ascii.\n- Rewritten `parse_address` util added test\n- Removed redundant super() arguments\n- Simplify `futures` import in gthread module\n- Fixed worker_connections` setting to also affects the Gthread worker type\n- Fixed setting max_requests\n- Bump minimum Eventlet and Gevent versions to 0.24 and 1.4\n- Use Python default SSL cipher list by default\n- handle `wsgi.input_terminated` extension\n- Simplify Paste Deployment documentation\n- Fix root logging: root and logger are same level.\n- Fixed typo in ssl_version documentation\n- Documented  systemd deployment unit examples\n- Added systemd sd_notify support\n- Fixed typo in gthread.py\n- Added `tornado <https://www.tornadoweb.org/>`_ 5 and  6 support\n- Declare our setuptools dependency\n- Added support to `--bind` to open file descriptors\n- Document how to serve WSGI app modules from Gunicorn\n- Provide guidance on X-Forwarded-For access log in documentation\n- Add support for named constants in the `--ssl-version` flag\n- Clarify log format usage of header & environment in documentation\n- Fixed systemd documentation to properly setup gunicorn unix socket\n- Prevent removal unix socket for reuse_port\n- Fix `ResourceWarning` when reading a Python config module\n- Remove unnecessary call to dict keys method\n- Support str and bytes for UNIX socket addresses\n- fixed `InotifyReloadeder`:  handle `module.__file__` is None\n- `/dev/shm` as a convenient alternative to making your own tmpfs mount in fchmod FAQ\n- fix examples to work on python3\n- Fix typo in `--max-requests` documentation\n- Clear tornado ioloop before os.fork\n- Miscellaneous fixes and improvement for linting using Pylint\n\n### Breaking Change\n\n- Removed gaiohttp worker\n- Drop support for Python 2.x\n- Drop support for EOL Python 3.2 and 3.3\n- Drop support for Paste Deploy server blocks\n"
  },
  {
    "path": "docs/content/2020-news.md",
    "content": "<span id=\"news-2020\"></span>\n# Changelog - 2020\n\n!!! note\n    Please see [news](news.md) for the latest changes\n\n\n"
  },
  {
    "path": "docs/content/2021-news.md",
    "content": "<span id=\"news-2021\"></span>\n# Changelog - 2021\n\n!!! note\n    Please see [news](news.md) for the latest changes\n\n\n## 20.1.0 - 2021-02-12\n\n- document WEB_CONCURRENCY is set by, at least, Heroku\n- capture peername from accept: Avoid calls to getpeername by capturing the peer name returned by\n  accept\n- log a warning when a worker was terminated due to a signal\n- fix tornado usage with latest versions of Django \n- add support for python -m gunicorn\n- fix systemd socket activation example\n- allows to set wsgi application in config file using `wsgi_app`\n- document `--timeout = 0`\n- always close a connection when the number of requests exceeds the max requests\n- Disable keepalive during graceful shutdown\n- kill tasks in the gthread workers during upgrade\n- fix latency in gevent worker when accepting new requests\n- fix file watcher: handle errors when new worker reboot and ensure the list of files is kept\n- document the default name and path of the configuration file\n- document how variable impact configuration\n- document the `$PORT` environment variable\n- added milliseconds option to request_time in access_log\n- added PIP requirements to be used for example\n- remove version from the Server header\n- fix sendfile: use `socket.sendfile` instead of `os.sendfile`\n- reloader: use  absolute path to prevent empty to prevent0 `InotifyError` when a file \n  is added to the working directory\n- Add --print-config option to print the resolved settings at startup.\n- remove the `--log-dict-config` CLI flag because it never had a working format\n  (the `logconfig_dict` setting in configuration files continues to work)\n\n\n### Breaking changes\n\n- minimum version is Python 3.5\n- remove version from the Server header \n\n** Documentation **\n\n\n\n** Others **\n\n- miscellaneous changes in the code base to be a better citizen with Python 3\n- remove dead code\n- fix documentation generation\n"
  },
  {
    "path": "docs/content/2023-news.md",
    "content": "<span id=\"news-2023\"></span>\n# Changelog - 2023\n\n## 21.2.0 - 2023-07-19\n\n- fix thread worker: revert change considering connection as idle . \n\n!!! note\n    This is fixing the bad file description error.\n    \n    21.1.0 - 2023-07-18\n\n\n===================\n\n- fix thread worker: fix socket removal from the queue\n\n## 21.0.1 - 2023-07-17\n\n- fix documentation build\n\n## 21.0.0 - 2023-07-17\n\n- support python 3.11\n- fix gevent and eventlet workers\n- fix threads support (gththread.md): improve performance and unblock requests\n- SSL: now use SSLContext object\n- HTTP parser: miscellaneous fixes\n- remove unnecessary setuid calls\n- fix testing\n- improve logging\n- miscellaneous fixes to core engine\n\n*** RELEASE NOTE ***\n\nWe made this release major to start our new release cycle. More info will be provided on our discussion forum.\n"
  },
  {
    "path": "docs/content/2024-news.md",
    "content": "<span id=\"news-2024\"></span>\n# Changelog - 2024\n\n## 23.0.0 - 2024-08-10\n\n- minor docs fixes ([PR #3217](https://github.com/benoitc/gunicorn/pull/3217), [PR #3089](https://github.com/benoitc/gunicorn/pull/3089), [PR #3167](https://github.com/benoitc/gunicorn/pull/3167))\n- worker_class parameter accepts a class ([PR #3079](https://github.com/benoitc/gunicorn/pull/3079))\n- fix deadlock if request terminated during chunked parsing ([PR #2688](https://github.com/benoitc/gunicorn/pull/2688))\n- permit receiving Transfer-Encodings: compress, deflate, gzip ([PR #3261](https://github.com/benoitc/gunicorn/pull/3261))\n- permit Transfer-Encoding headers specifying multiple encodings. note: no parameters, still ([PR #3261](https://github.com/benoitc/gunicorn/pull/3261))\n- sdist generation now explicitly excludes sphinx build folder ([PR #3257](https://github.com/benoitc/gunicorn/pull/3257))\n- decode bytes-typed status (as can be passed by gevent) as utf-8 instead of raising `TypeError` ([PR #2336](https://github.com/benoitc/gunicorn/pull/2336))\n- raise correct Exception when encounting invalid chunked requests ([PR #3258](https://github.com/benoitc/gunicorn/pull/3258))\n- the SCRIPT_NAME and PATH_INFO headers, when received from allowed forwarders, are no longer restricted for containing an underscore ([PR #3192](https://github.com/benoitc/gunicorn/pull/3192))\n- include IPv6 loopback address ``[::1]`` in default for [forwarded-allow-ips](reference/settings.md#forwarded_allow_ips) and [proxy-allow-ips](reference/settings.md#proxy_allow_ips) ([PR #3192](https://github.com/benoitc/gunicorn/pull/3192))\n\n!!! note\n    - The SCRIPT_NAME change mitigates a regression that appeared first in the 22.0.0 release\n    - Review your [forwarded-allow-ips](reference/settings.md#forwarded_allow_ips) setting if you are still not seeing the SCRIPT_NAME transmitted\n    - Review your [forwarder-headers](reference/settings.md#forwarder_headers) setting if you are missing headers after upgrading from a version prior to 22.0.0\n\n\n### Breaking changes\n\n- refuse requests where the uri field is empty ([PR #3255](https://github.com/benoitc/gunicorn/pull/3255))\n- refuse requests with invalid CR/LR/NUL in heade field values ([PR #3253](https://github.com/benoitc/gunicorn/pull/3253))\n- remove temporary ``--tolerate-dangerous-framing`` switch from 22.0 ([PR #3260](https://github.com/benoitc/gunicorn/pull/3260))\n- If any of the breaking changes affect you, be aware that now refused requests can post a security problem, especially so in setups involving request pipe-lining and/or proxies.\n\n## 22.0.0 - 2024-04-17\n\n- use `utime` to notify workers liveness \n- migrate setup to pyproject.toml\n- fix numerous security vulnerabilities in HTTP parser (closing some request smuggling vectors)\n- parsing additional requests is no longer attempted past unsupported request framing\n- on HTTP versions < 1.1 support for chunked transfer is refused (only used in exploits)\n- requests conflicting configured or passed SCRIPT_NAME now produce a verbose error\n- Trailer fields are no longer inspected for headers indicating secure scheme\n- support Python 3.12\n\n### Breaking changes\n\n- minimum version is Python 3.7\n- the limitations on valid characters in the HTTP method have been bounded to Internet Standards\n- requests specifying unsupported transfer coding (order.md) are refused by default (rare.md)\n- HTTP methods are no longer casefolded by default (IANA method registry contains none affected)\n- HTTP methods containing the number sign (#) are no longer accepted by default (rare.md)\n- HTTP versions < 1.0 or >= 2.0 are no longer accepted by default (rare, only HTTP/1.1 is supported)\n- HTTP versions consisting of multiple digits or containing a prefix/suffix are no longer accepted\n- HTTP header field names Gunicorn cannot safely map to variables are silently dropped, as in other software\n- HTTP headers with empty field name are refused by default (no legitimate use cases, used in exploits)\n- requests with both Transfer-Encoding and Content-Length are refused by default (such a message might indicate an attempt to perform request smuggling)\n- empty transfer codings are no longer permitted (reportedly seen with really old & broken proxies)\n\n\n### Security\n\n- fix CVE-2024-1135\n"
  },
  {
    "path": "docs/content/2026-news.md",
    "content": "<span id=\"news-2026\"></span>\n# Changelog - 2026\n\n## 25.1.0 - 2026-02-13\n\n### New Features\n\n- **Control Interface (gunicornc)**: Add interactive control interface for managing\n  running Gunicorn instances, similar to birdc for BIRD routing daemon\n  ([PR #3505](https://github.com/benoitc/gunicorn/pull/3505))\n  - Unix socket-based communication with JSON protocol\n  - Interactive mode with readline support and command history\n  - Commands: `show all/workers/dirty/config/stats/listeners`\n  - Worker management: `worker add/remove/kill`, `dirty add/remove`\n  - Server control: `reload`, `reopen`, `shutdown`\n  - New settings: `--control-socket`, `--control-socket-mode`, `--no-control-socket`\n  - New CLI tool: `gunicornc` for connecting to control socket\n  - See [Control Interface Guide](guides/gunicornc.md) for details\n\n- **Dirty Stash**: Add global shared state between workers via `dirty.stash`\n  ([PR #3503](https://github.com/benoitc/gunicorn/pull/3503))\n  - In-memory key-value store accessible by all workers\n  - Supports get, set, delete, clear, keys, and has operations\n  - Useful for sharing state like feature flags, rate limits, or cached data\n\n- **Dirty Binary Protocol**: Implement efficient binary protocol for dirty arbiter IPC\n  using TLV (Type-Length-Value) encoding\n  ([PR #3500](https://github.com/benoitc/gunicorn/pull/3500))\n  - More efficient than JSON for binary data\n  - Supports all Python types: str, bytes, int, float, bool, None, list, dict\n  - Better performance for large payloads\n\n- **Dirty TTIN/TTOU Signals**: Add dynamic worker scaling for dirty arbiters\n  ([PR #3504](https://github.com/benoitc/gunicorn/pull/3504))\n  - Send SIGTTIN to increase dirty workers\n  - Send SIGTTOU to decrease dirty workers\n  - Respects minimum worker constraints from app configurations\n\n### Changes\n\n- **ASGI Worker**: Promoted from beta to stable\n- **Dirty Arbiters**: Now marked as beta feature\n\n### Documentation\n\n- Fix Markdown formatting in /configure documentation\n\n---\n\n## 25.0.3 - 2026-02-07\n\n### Bug Fixes\n\n- Fix RuntimeError when StopIteration is raised inside ASGI response body\n  coroutine (PEP 479 compliance)\n\n- Fix deprecation warning for passing maxsplit as positional argument in\n  `re.split()` (Python 3.13+)\n\n---\n\n## 25.0.2 - 2026-02-06\n\n### Bug Fixes\n\n- Fix ASGI concurrent request failures through nginx proxy by normalizing\n  sockaddr tuples to handle both 2-tuple (IPv4) and 4-tuple (IPv6) formats\n  ([PR #3485](https://github.com/benoitc/gunicorn/pull/3485))\n\n- Fix graceful disconnect handling for ASGI worker to properly handle\n  client disconnects without raising exceptions\n  ([PR #3485](https://github.com/benoitc/gunicorn/pull/3485))\n\n- Fix lazy import of dirty module for gevent compatibility - prevents\n  import errors when concurrent.futures is imported before gevent monkey-patching\n  ([PR #3483](https://github.com/benoitc/gunicorn/pull/3483))\n\n### Changes\n\n- Refactor: Extract `_normalize_sockaddr` utility function for consistent\n  socket address handling across workers\n\n- Add license headers to all Python source files\n\n- Update copyright year to 2026 in LICENSE and NOTICE files\n\n---\n\n## 25.0.1 - 2026-02-02\n\n### Bug Fixes\n\n- Fix ASGI streaming responses (SSE) hanging: add chunked transfer encoding for\n  HTTP/1.1 responses without Content-Length header. Without chunked encoding,\n  clients wait for connection close to determine end-of-response.\n\n### Changes\n\n- Update celery_alternative example to use FastAPI with native ASGI worker and\n  uvloop for async task execution\n\n### Testing\n\n- Add ASGI compliance test suite with Docker-based integration tests covering HTTP,\n  WebSocket, streaming, lifespan, framework integration (Starlette, FastAPI),\n  HTTP/2, and concurrency scenarios\n\n---\n\n## 25.0.0 - 2026-02-01\n\n### New Features\n\n- **Dirty Arbiters**: Separate process pool for executing long-running, blocking\n  operations (AI model loading, heavy computation) without blocking HTTP workers\n  ([PR #3460](https://github.com/benoitc/gunicorn/pull/3460))\n  - Inspired by Erlang's dirty schedulers\n  - Asyncio-based with Unix socket IPC\n  - Stateful workers that persist loaded resources\n  - New settings: `--dirty-app`, `--dirty-workers`, `--dirty-timeout`,\n    `--dirty-threads`, `--dirty-graceful-timeout`\n  - Lifecycle hooks: `on_dirty_starting`, `dirty_post_fork`,\n    `dirty_worker_init`, `dirty_worker_exit`\n\n- **Per-App Worker Allocation for Dirty Arbiters**: Control how many dirty workers\n  load each app for memory optimization with heavy models\n  ([PR #3473](https://github.com/benoitc/gunicorn/pull/3473))\n  - Set `workers` class attribute on DirtyApp (e.g., `workers = 2`)\n  - Or use config format `module:class:N` (e.g., `myapp:HeavyModel:2`)\n  - Requests automatically routed to workers with the target app\n  - New exception `DirtyNoWorkersAvailableError` for graceful error handling\n  - Example: 8 workers × 10GB model = 80GB → with `workers=2`: 20GB (75% savings)\n\n- **HTTP/2 Support (Beta)**: Native HTTP/2 (RFC 7540) support for improved performance\n  with modern clients ([PR #3468](https://github.com/benoitc/gunicorn/pull/3468))\n  - Multiplexed streams over a single connection\n  - Header compression (HPACK)\n  - Flow control and stream prioritization\n  - Works with gthread, gevent, and ASGI workers\n  - New settings: `--http-protocols`, `--http2-max-concurrent-streams`,\n    `--http2-initial-window-size`, `--http2-max-frame-size`, `--http2-max-header-list-size`\n  - Requires SSL/TLS and h2 library: `pip install gunicorn[http2]`\n  - See [HTTP/2 Guide](guides/http2.md) for details\n  - New example: `examples/http2_gevent/` with Docker and tests\n\n- **HTTP 103 Early Hints**: Support for RFC 8297 Early Hints to enable browsers to\n  preload resources before the final response\n  ([PR #3468](https://github.com/benoitc/gunicorn/pull/3468))\n  - WSGI: `environ['wsgi.early_hints'](headers)` callback\n  - ASGI: `http.response.informational` message type\n  - Works with both HTTP/1.1 and HTTP/2\n\n- **uWSGI Protocol for ASGI Worker**: The ASGI worker now supports receiving requests\n  via the uWSGI binary protocol from nginx\n  ([PR #3467](https://github.com/benoitc/gunicorn/pull/3467))\n\n### Bug Fixes\n\n- Fix HTTP/2 ALPN negotiation for gevent and eventlet workers when\n  `do_handshake_on_connect` is False (the default). The TLS handshake is now\n  explicitly performed before checking `selected_alpn_protocol()`.\n\n- Fix setproctitle initialization with systemd socket activation\n  ([#3465](https://github.com/benoitc/gunicorn/issues/3465))\n\n- Fix `Expect: 100-continue` handling: ignore the header for HTTP/1.0 requests\n  since 100-continue is only valid for HTTP/1.1+\n  ([PR #3463](https://github.com/benoitc/gunicorn/pull/3463))\n\n- Fix missing `_expected_100_continue` attribute in UWSGIRequest\n\n- Disable setproctitle on macOS to prevent segfaults during process title updates\n\n- Publish full exception traceback when the application fails to load\n  ([#3462](https://github.com/benoitc/gunicorn/issues/3462))\n\n### Deprecations\n\n- **Eventlet Worker**: The `eventlet` worker is deprecated and will be removed in\n  Gunicorn 26.0. Eventlet itself is [no longer actively maintained](https://eventlet.readthedocs.io/en/latest/asyncio/migration.html).\n  Please migrate to `gevent`, `gthread`, or another supported worker type.\n\n### Changes\n\n- Remove obsolete Makefile targets\n  ([PR #3471](https://github.com/benoitc/gunicorn/pull/3471))\n\n---\n\n## 24.1.1 - 2026-01-24\n\n### Bug Fixes\n\n- Fix `forwarded_allow_ips` and `proxy_allow_ips` to remain as strings for backward\n  compatibility with external tools like uvicorn. Network validation now uses strict\n  mode to detect invalid CIDR notation (e.g., `192.168.1.1/24` where host bits are set)\n  ([#3458](https://github.com/benoitc/gunicorn/issues/3458),\n  [PR #3459](https://github.com/benoitc/gunicorn/pull/3459))\n\n---\n\n## 24.1.0 - 2026-01-23\n\n### New Features\n\n- **Official Docker Image**: Gunicorn now publishes official Docker images to GitHub\n  Container Registry at `ghcr.io/benoitc/gunicorn`\n  - Based on Python 3.12 slim image\n  - Uses recommended worker formula (2 × CPU + 1)\n  - Configurable via environment variables\n\n- **PROXY Protocol v2 Support**: Extended PROXY protocol implementation to support\n  the binary v2 format in addition to the existing text-based v1 format\n  ([PR #3451](https://github.com/benoitc/gunicorn/pull/3451))\n  - New `--proxy-protocol` modes: `off`, `v1`, `v2`, `auto`\n  - `auto` mode (default when enabled) detects v1 or v2 automatically\n  - v2 binary format is more efficient and supports additional metadata\n  - Works with HAProxy, AWS NLB/ALB, and other PROXY protocol v2 sources\n\n- **CIDR Network Support**: `--forwarded-allow-ips` and `--proxy-allow-from` now\n  accept CIDR notation (e.g., `192.168.0.0/16`) for specifying trusted networks\n  ([PR #3449](https://github.com/benoitc/gunicorn/pull/3449))\n\n- **Socket Backlog Metric**: New `gunicorn.socket.backlog` gauge metric reports\n  the current socket backlog size on Linux systems\n  ([PR #3450](https://github.com/benoitc/gunicorn/pull/3450))\n\n- **InotifyReloader Enhancement**: The inotify-based reloader now watches newly\n  imported modules, not just those loaded at startup\n  ([PR #3447](https://github.com/benoitc/gunicorn/pull/3447))\n\n### Bug Fixes\n\n- Fix signal handling regression where SIGCLD alias caused \"Unhandled signal: cld\"\n  errors on Linux when workers fail during boot\n  ([#3453](https://github.com/benoitc/gunicorn/discussions/3453))\n\n- Fix socket blocking mode on keepalive connections preventing SSL handshake\n  failures with async workers\n  ([PR #3452](https://github.com/benoitc/gunicorn/pull/3452))\n\n- Use smaller buffer size in `finish_body()` for faster timeout detection on\n  slow or abandoned connections\n  ([PR #3453](https://github.com/benoitc/gunicorn/pull/3453))\n\n- Handle `SSLWantReadError` in `finish_body()` to prevent worker hangs during\n  SSL renegotiation\n  ([PR #3448](https://github.com/benoitc/gunicorn/pull/3448))\n\n- Log SIGTERM as info level instead of warning to reduce noise in orchestrated\n  environments\n  ([PR #3446](https://github.com/benoitc/gunicorn/pull/3446))\n\n- Print exception details to stderr when worker fails to boot\n  ([PR #3443](https://github.com/benoitc/gunicorn/pull/3443))\n\n- Fix `unreader.unread()` to prepend data to buffer instead of appending\n  ([PR #3442](https://github.com/benoitc/gunicorn/pull/3442))\n\n- Prevent `RecursionError` when pickling Config objects\n  ([PR #3441](https://github.com/benoitc/gunicorn/pull/3441))\n\n- Use proper exception chaining with `raise from` in glogging.py\n  ([PR #3440](https://github.com/benoitc/gunicorn/pull/3440))\n\n---\n\n## 24.0.0 - 2026-01-23\n\n### New Features\n\n- **ASGI Worker (Beta)**: Native asyncio-based ASGI support for running async Python\n  frameworks like FastAPI, Starlette, and Quart without external dependencies\n  ([PR #3444](https://github.com/benoitc/gunicorn/pull/3444))\n  - HTTP/1.1 with keepalive connections\n  - WebSocket support\n  - Lifespan protocol for startup/shutdown hooks\n  - Optional uvloop for improved performance\n  - New settings: `--asgi-loop`, `--asgi-lifespan`, `--root-path`\n\n- **uWSGI Binary Protocol**: Support for receiving requests from nginx via\n  `uwsgi_pass` directive, enabling efficient binary protocol communication\n  ([PR #3444](https://github.com/benoitc/gunicorn/pull/3444))\n  - New settings: `--protocol uwsgi`, `--uwsgi-allow-from`\n\n- **Documentation Migration**: Migrated documentation from Sphinx to MkDocs\n  with Material theme for improved navigation and mobile experience\n  ([PR #3426](https://github.com/benoitc/gunicorn/pull/3426))\n\n### Security\n\n- **eventlet**: Require eventlet >= 0.40.3 to address CVE-2021-21419 (websocket\n  memory exhaustion) and CVE-2025-58068 (HTTP request smuggling)\n  ([PR #3445](https://github.com/benoitc/gunicorn/pull/3445))\n\n- **gevent**: Require gevent >= 24.10.1 to address CVE-2023-41419 (HTTP request\n  smuggling) and CVE-2024-3219 (socket.socketpair vulnerability)\n  ([PR #3445](https://github.com/benoitc/gunicorn/pull/3445))\n\n- **tornado**: Require tornado >= 6.5.0 to address CVE-2025-47287 (HTTP request\n  smuggling) and other security fixes\n  ([PR #3445](https://github.com/benoitc/gunicorn/pull/3445))\n\n### Changes\n\n- Documentation now hosted at https://gunicorn.org\n- Updated license configuration to PEP 639 format for uv compatibility\n\n!!! warning \"ASGI Worker Beta\"\n    The ASGI worker is a beta feature. While tested, the API and behavior\n    may change in future releases. Please report any issues on GitHub.\n"
  },
  {
    "path": "docs/content/404.md",
    "content": "# Page Not Found\n\nThe page you're looking for doesn't exist or has moved.\n\n<div class=\"quick-links\" style=\"margin-top: 2rem;\">\n  <a href=\"/\" class=\"quick-link\">\n    <strong>Home</strong>\n    <span>Return to the homepage</span>\n  </a>\n  <a href=\"/quickstart/\" class=\"quick-link\">\n    <strong>Quickstart</strong>\n    <span>Get started with Gunicorn</span>\n  </a>\n  <a href=\"/reference/settings/\" class=\"quick-link\">\n    <strong>Settings</strong>\n    <span>Configuration reference</span>\n  </a>\n  <a href=\"https://github.com/benoitc/gunicorn/issues\" class=\"quick-link\">\n    <strong>Report Issue</strong>\n    <span>Let us know about broken links</span>\n  </a>\n</div>\n"
  },
  {
    "path": "docs/content/CNAME",
    "content": "gunicorn.org"
  },
  {
    "path": "docs/content/asgi.md",
    "content": "# ASGI Worker\n\nGunicorn includes a native ASGI worker that enables running async Python web frameworks\nlike FastAPI, Starlette, and Quart without external dependencies like Uvicorn.\n\n## Quick Start\n\n```bash\n# Install gunicorn\npip install gunicorn\n\n# Run an ASGI application\ngunicorn myapp:app --worker-class asgi --workers 4\n```\n\nFor FastAPI applications:\n\n```bash\ngunicorn main:app --worker-class asgi --bind 0.0.0.0:8000\n```\n\n## Features\n\nThe ASGI worker provides:\n\n- **HTTP/1.1** with keepalive connections\n- **WebSocket** support for real-time applications\n- **Lifespan protocol** for startup/shutdown hooks\n- **Optional uvloop** for improved performance\n- **SSL/TLS** support\n- **uWSGI protocol** for nginx `uwsgi_pass` integration\n\n## Configuration\n\n### Worker Class\n\nSet the worker class to `asgi`:\n\n```bash\ngunicorn myapp:app --worker-class asgi\n```\n\nOr in a configuration file:\n\n```python\n# gunicorn.conf.py\nworker_class = \"asgi\"\n```\n\n### Event Loop\n\nControl which asyncio event loop implementation to use:\n\n| Value    | Description |\n|----------|-------------|\n| `auto`   | Use uvloop if available, otherwise asyncio (default) |\n| `asyncio`| Use Python's built-in asyncio event loop |\n| `uvloop` | Use uvloop (must be installed separately) |\n\n```bash\ngunicorn myapp:app --worker-class asgi --asgi-loop uvloop\n```\n\nTo use uvloop, install it first:\n\n```bash\npip install uvloop\n```\n\n### Lifespan Protocol\n\nThe lifespan protocol lets your application run code at startup and shutdown.\nThis is essential for frameworks that need to initialize database connections,\ncaches, or background tasks.\n\n| Value  | Description |\n|--------|-------------|\n| `auto` | Detect if app supports lifespan, enable if so (default) |\n| `on`   | Always run lifespan protocol (fail if unsupported) |\n| `off`  | Never run lifespan protocol |\n\n```bash\ngunicorn myapp:app --worker-class asgi --asgi-lifespan on\n```\n\nExample FastAPI application using lifespan:\n\n```python\nfrom contextlib import asynccontextmanager\nfrom fastapi import FastAPI\n\n@asynccontextmanager\nasync def lifespan(app: FastAPI):\n    # Startup: initialize resources\n    print(\"Starting up...\")\n    yield\n    # Shutdown: cleanup resources\n    print(\"Shutting down...\")\n\napp = FastAPI(lifespan=lifespan)\n```\n\n### Root Path\n\nWhen running behind a reverse proxy that mounts your application at a subpath,\nset `root_path` so your application knows its mount point:\n\n```bash\ngunicorn myapp:app --worker-class asgi --root-path /api\n```\n\nThis is equivalent to the `SCRIPT_NAME` in WSGI applications.\n\n### Worker Connections\n\nControl the maximum number of concurrent connections per worker:\n\n```bash\ngunicorn myapp:app --worker-class asgi --worker-connections 1000\n```\n\n!!! note\n    Unlike sync workers, the `--threads` option has no effect on ASGI workers.\n    Use `--worker-connections` to control concurrency.\n\n## WebSocket Support\n\nThe ASGI worker supports WebSocket connections out of the box. No additional\nconfiguration is required.\n\nExample with Starlette:\n\n```python\nfrom starlette.applications import Starlette\nfrom starlette.routing import WebSocketRoute\n\nasync def websocket_endpoint(websocket):\n    await websocket.accept()\n    while True:\n        data = await websocket.receive_text()\n        await websocket.send_text(f\"Echo: {data}\")\n\napp = Starlette(routes=[\n    WebSocketRoute(\"/ws\", websocket_endpoint),\n])\n```\n\n## Production Deployment\n\n### With Nginx (HTTP Proxy)\n\n```nginx\nupstream gunicorn {\n    server 127.0.0.1:8000;\n}\n\nserver {\n    listen 80;\n    server_name example.com;\n\n    location / {\n        proxy_pass http://gunicorn;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n    }\n\n    # WebSocket support\n    location /ws {\n        proxy_pass http://gunicorn;\n        proxy_http_version 1.1;\n        proxy_set_header Upgrade $http_upgrade;\n        proxy_set_header Connection \"upgrade\";\n        proxy_set_header Host $host;\n    }\n}\n```\n\n### With Nginx (uWSGI Protocol)\n\nFor better performance, you can use nginx's native uWSGI protocol support:\n\n```bash\ngunicorn myapp:app --worker-class asgi --protocol uwsgi --bind 127.0.0.1:8000\n```\n\n```nginx\nupstream gunicorn {\n    server 127.0.0.1:8000;\n}\n\nserver {\n    listen 80;\n    server_name example.com;\n\n    location / {\n        uwsgi_pass gunicorn;\n        include uwsgi_params;\n    }\n}\n```\n\n!!! note\n    WebSocket connections are not supported when using the uWSGI protocol.\n    Use HTTP proxy for WebSocket endpoints.\n\nSee [uWSGI Protocol](uwsgi.md) for more details on uWSGI protocol configuration.\n\n### Recommended Settings\n\nFor production ASGI deployments:\n\n```python\n# gunicorn.conf.py\nworker_class = \"asgi\"\nworkers = 4  # Number of worker processes\nworker_connections = 1000  # Max connections per worker\nkeepalive = 5  # Keepalive timeout\ntimeout = 120  # Worker timeout\ngraceful_timeout = 30  # Graceful shutdown timeout\n\n# Performance tuning\nasgi_loop = \"auto\"  # Use uvloop if available\nasgi_lifespan = \"auto\"  # Auto-detect lifespan support\n```\n\n## Comparison with Other ASGI Servers\n\n| Feature | Gunicorn ASGI | Uvicorn | Hypercorn |\n|---------|---------------|---------|-----------|\n| Process management | Built-in | External | Built-in |\n| HTTP/2 | Yes | No | Yes |\n| WebSocket | Yes | Yes | Yes |\n| Lifespan | Yes | Yes | Yes |\n| uvloop support | Yes | Yes | Yes |\n\n!!! note\n    HTTP/2 requires SSL/TLS and the h2 library. See [HTTP/2 Support](guides/http2.md) for details.\n\nGunicorn's ASGI worker provides the same process management, logging, and\nconfiguration capabilities you're familiar with from WSGI deployments.\n\n## Troubleshooting\n\n### Lifespan startup failed\n\nIf you see \"ASGI lifespan startup failed\", your application may not properly\nimplement the lifespan protocol. Either fix the application or set\n`--asgi-lifespan off`.\n\n### Connection limits\n\nIf you're hitting connection limits, increase `--worker-connections` or add\nmore workers with `--workers`.\n\n### Slow responses under load\n\nTry using uvloop for better performance:\n\n```bash\npip install uvloop\ngunicorn myapp:app --worker-class asgi --asgi-loop uvloop\n```\n\n## See Also\n\n- [Settings Reference](reference/settings.md#asgi_loop) - All ASGI-related settings\n- [Deploy](deploy.md) - General deployment guidance\n- [Design](design.md) - Worker architecture overview\n"
  },
  {
    "path": "docs/content/assets/javascripts/toc-collapse.js",
    "content": "// Collapsible TOC for settings page\n(function() {\n  function initCollapsibleTOC() {\n    // Only apply to pages with many TOC items (like settings)\n    var tocNav = document.querySelector('.md-nav--secondary');\n    if (!tocNav) return;\n\n    // Skip if already initialized\n    if (tocNav.dataset.tocCollapse === 'true') return;\n    tocNav.dataset.tocCollapse = 'true';\n\n    var tocItems = tocNav.querySelectorAll('.md-nav__item');\n    if (tocItems.length < 20) return;\n\n    // Find all top-level TOC items that have nested lists\n    var topList = tocNav.querySelector('.md-nav__list');\n    if (!topList) return;\n\n    var sections = topList.children;\n\n    for (var i = 0; i < sections.length; i++) {\n      (function(section) {\n        var nestedNav = section.querySelector('.md-nav');\n        if (!nestedNav) return;\n\n        var link = section.querySelector('.md-nav__link');\n        if (!link) return;\n\n        // Skip if already has toggle\n        if (link.querySelector('.toc-toggle')) return;\n\n        // Collapse by default\n        nestedNav.style.display = 'none';\n\n        // Create toggle button\n        var toggle = document.createElement('span');\n        toggle.className = 'toc-toggle';\n        toggle.innerHTML = '+';\n        toggle.style.float = 'right';\n        toggle.style.marginRight = '0.5rem';\n        toggle.style.fontWeight = 'bold';\n        toggle.style.cursor = 'pointer';\n        toggle.style.userSelect = 'none';\n        link.appendChild(toggle);\n\n        // Toggle function for this specific section\n        function toggleSection(e) {\n          if (e) {\n            e.preventDefault();\n            e.stopPropagation();\n          }\n\n          if (nestedNav.style.display === 'none') {\n            nestedNav.style.display = 'block';\n            toggle.innerHTML = '−';\n          } else {\n            nestedNav.style.display = 'none';\n            toggle.innerHTML = '+';\n          }\n        }\n\n        // Click on toggle button\n        toggle.onclick = toggleSection;\n      })(sections[i]);\n    }\n  }\n\n  // Run on DOM ready\n  if (document.readyState === 'loading') {\n    document.addEventListener('DOMContentLoaded', initCollapsibleTOC);\n  } else {\n    initCollapsibleTOC();\n  }\n\n  // Re-run on instant navigation (MkDocs Material)\n  if (typeof document$ !== 'undefined') {\n    document$.subscribe(initCollapsibleTOC);\n  }\n})();\n"
  },
  {
    "path": "docs/content/assets/stylesheets/home.css",
    "content": "/* ============================================\n   Gunicorn Landing Page\n   Inspired by Caddy: minimal, spacious, clean\n   ============================================ */\n\n.home {\n  --accent: #00a650;\n  --accent-hover: #00c853;\n  --accent-dark: #008542;\n  --teal: #00bfa5;\n  --text: #1a1a2e;\n  --text-muted: #555;\n  --bg: #fff;\n  --bg-alt: #f8faf8;\n  --border: #e0e6e0;\n  --code-bg: #0d1117;\n  --max-width: 900px;\n\n  width: 100%;\n  max-width: none;\n  margin: 0;\n  padding: 0;\n  font-size: 1.0625rem;\n  line-height: 1.7;\n  color: var(--text);\n}\n\n[data-md-color-scheme=\"slate\"] .home {\n  --text: #e6e6e6;\n  --text-muted: #aaa;\n  --bg: #0d1117;\n  --bg-alt: #161b22;\n  --border: #30363d;\n}\n\n/* Remove MkDocs constraints */\n.md-main__inner { margin: 0; max-width: none; }\n.md-content { max-width: none; }\n.md-content__inner { margin: 0; padding: 0; }\n\n/* ============================================\n   Sections - Caddy-style vertical flow\n   ============================================ */\n.home section {\n  padding: 5rem 2rem;\n}\n\n.home section:nth-child(even) {\n  background: var(--bg-alt);\n}\n\n.home .container {\n  max-width: var(--max-width);\n  margin: 0 auto;\n}\n\n/* ============================================\n   Hero\n   ============================================ */\n.hero {\n  text-align: center;\n  padding: 6rem 2rem 5rem;\n}\n\n.hero .container {\n  max-width: 700px;\n}\n\n.hero__logo {\n  width: 350px !important;\n  max-width: 350px !important;\n  min-width: 350px;\n  height: auto;\n  margin-bottom: 2rem;\n}\n\n.hero h1 {\n  font-size: 3rem;\n  font-weight: 700;\n  line-height: 1.15;\n  margin: 0 0 1.5rem 0;\n  letter-spacing: -0.02em;\n  white-space: nowrap;\n}\n\n.hero__tagline {\n  font-size: 1.25rem;\n  color: var(--text-muted);\n  margin: 0 0 2.5rem 0;\n  max-width: 550px;\n  margin-left: auto;\n  margin-right: auto;\n}\n\n.hero__buttons {\n  display: flex;\n  gap: 1rem;\n  justify-content: center;\n  flex-wrap: wrap;\n  margin-bottom: 3rem;\n}\n\n.btn {\n  display: inline-flex;\n  align-items: center;\n  gap: 0.5rem;\n  padding: 0.875rem 1.75rem;\n  font-size: 1rem;\n  font-weight: 500;\n  text-decoration: none;\n  border-radius: 6px;\n  transition: all 0.15s ease;\n}\n\n.btn--primary {\n  background: linear-gradient(135deg, var(--accent) 0%, var(--accent-hover) 100%);\n  color: #fff;\n  box-shadow: 0 4px 12px rgba(0, 166, 80, 0.3);\n}\n\n.btn--primary:hover {\n  box-shadow: 0 6px 20px rgba(0, 166, 80, 0.4);\n  transform: translateY(-2px);\n}\n\n.btn--secondary {\n  background: transparent;\n  color: var(--text);\n  border: 1px solid var(--border);\n}\n\n.btn--secondary:hover {\n  border-color: var(--accent);\n  color: var(--accent);\n}\n\n/* Terminal */\n.terminal {\n  background: var(--code-bg);\n  border-radius: 8px;\n  overflow: hidden;\n  text-align: left;\n  max-width: 500px;\n  margin: 0 auto;\n  box-shadow: 0 8px 30px rgba(0,0,0,0.12);\n}\n\n.terminal__header {\n  background: #161b22;\n  padding: 0.75rem 1rem;\n  display: flex;\n  gap: 6px;\n}\n\n.terminal__dot {\n  width: 12px;\n  height: 12px;\n  border-radius: 50%;\n}\n\n.terminal__dot--red { background: #ff5f56; }\n.terminal__dot--yellow { background: #ffbd2e; }\n.terminal__dot--green { background: #27c93f; }\n\n.terminal__body {\n  padding: 1.25rem 1.5rem;\n  font-family: 'SF Mono', Monaco, Consolas, monospace;\n  font-size: 0.9rem;\n  line-height: 1.8;\n  color: #c9d1d9;\n}\n\n.terminal__line {\n  display: block;\n}\n\n.terminal__prompt {\n  color: var(--accent-hover);\n  user-select: none;\n}\n\n.terminal__comment {\n  color: #6e7681;\n}\n\n/* ============================================\n   Why Gunicorn - 3 pillars\n   ============================================ */\n.why h2 {\n  text-align: center;\n  font-size: 2rem;\n  margin: 0 0 3rem 0;\n}\n\n.pillars {\n  display: grid;\n  grid-template-columns: repeat(3, 1fr);\n  gap: 2rem;\n}\n\n.pillar h3 {\n  font-size: 1.125rem;\n  margin: 0 0 0.5rem 0;\n}\n\n.pillar p {\n  color: var(--text-muted);\n  margin: 0;\n  font-size: 0.9375rem;\n}\n\n/* ============================================\n   Frameworks\n   ============================================ */\n.frameworks h2 {\n  text-align: center;\n  font-size: 1.75rem;\n  margin: 0 0 0.5rem 0;\n}\n\n.frameworks__subtitle {\n  text-align: center;\n  color: var(--text-muted);\n  margin: 0 0 2rem 0;\n}\n\n.frameworks__list {\n  display: flex;\n  flex-wrap: wrap;\n  justify-content: center;\n  gap: 0.75rem;\n}\n\n.framework-tag {\n  padding: 0.5rem 1rem;\n  background: var(--bg);\n  border: 1px solid var(--border);\n  border-radius: 100px;\n  font-size: 0.875rem;\n  font-weight: 500;\n  transition: all 0.15s ease;\n}\n\n[data-md-color-scheme=\"slate\"] .framework-tag {\n  background: var(--bg-alt);\n}\n\n.framework-tag:hover {\n  border-color: var(--accent);\n  color: var(--accent);\n}\n\n.framework-tag--new {\n  background: var(--accent);\n  color: #fff;\n  border-color: var(--accent);\n}\n\n/* ============================================\n   Workers\n   ============================================ */\n.workers h2 {\n  font-size: 1.75rem;\n  margin: 0 0 2rem 0;\n}\n\n.workers__grid {\n  display: grid;\n  grid-template-columns: 1fr 1fr;\n  gap: 1rem;\n}\n\n.worker {\n  padding: 1.5rem;\n  background: var(--bg);\n  border: 1px solid var(--border);\n  border-radius: 8px;\n  text-decoration: none;\n  color: inherit;\n  transition: border-color 0.15s ease;\n}\n\n[data-md-color-scheme=\"slate\"] .worker {\n  background: var(--bg-alt);\n}\n\n.worker:hover {\n  border-color: var(--accent);\n}\n\n.worker h3 {\n  font-size: 1rem;\n  margin: 0 0 0.25rem 0;\n  display: flex;\n  align-items: center;\n  gap: 0.5rem;\n}\n\n.worker p {\n  color: var(--text-muted);\n  font-size: 0.875rem;\n  margin: 0;\n}\n\n.badge {\n  font-size: 0.625rem;\n  font-weight: 700;\n  padding: 0.125rem 0.375rem;\n  background: var(--accent);\n  color: #fff;\n  border-radius: 3px;\n  text-transform: uppercase;\n  letter-spacing: 0.05em;\n}\n\n/* ============================================\n   Quick Links\n   ============================================ */\n.quick-links {\n  text-align: center;\n}\n\n.quick-links h2 {\n  font-size: 1.75rem;\n  margin: 0 0 2rem 0;\n}\n\n.quick-links__grid {\n  display: grid;\n  grid-template-columns: repeat(4, 1fr);\n  gap: 1rem;\n  text-align: left;\n}\n\n.quick-link {\n  padding: 1.25rem;\n  background: var(--bg);\n  border: 1px solid var(--border);\n  border-radius: 8px;\n  text-decoration: none;\n  color: inherit;\n  transition: border-color 0.15s ease;\n}\n\n[data-md-color-scheme=\"slate\"] .quick-link {\n  background: var(--bg-alt);\n}\n\n.quick-link:hover {\n  border-color: var(--accent);\n}\n\n.quick-link strong {\n  display: block;\n  margin-bottom: 0.25rem;\n}\n\n.quick-link span {\n  font-size: 0.875rem;\n  color: var(--text-muted);\n}\n\n/* ============================================\n   Sponsors\n   ============================================ */\n.sponsors {\n  text-align: center;\n}\n\n.sponsors h2 {\n  font-size: 1.75rem;\n  margin: 0 0 0.5rem 0;\n}\n\n.sponsors p {\n  color: var(--text-muted);\n  margin: 0 0 2rem 0;\n}\n\n.sponsors__logos {\n  display: flex;\n  flex-wrap: wrap;\n  justify-content: center;\n  align-items: center;\n  gap: 2rem;\n  margin-bottom: 2rem;\n  min-height: 60px;\n}\n\n.sponsors__logos img {\n  max-height: 50px;\n  max-width: 150px;\n  filter: grayscale(100%);\n  opacity: 0.7;\n  transition: all 0.15s ease;\n}\n\n.sponsors__logos img:hover {\n  filter: grayscale(0%);\n  opacity: 1;\n}\n\n[data-md-color-scheme=\"slate\"] .sponsors__logos img {\n  filter: grayscale(100%) brightness(1.5);\n}\n\n[data-md-color-scheme=\"slate\"] .sponsors__logos img:hover {\n  filter: grayscale(0%) brightness(1);\n}\n\n.sponsors__placeholder {\n  color: var(--text-muted);\n  font-size: 0.875rem;\n  padding: 1rem 2rem;\n  border: 2px dashed var(--border);\n  border-radius: 8px;\n}\n\n/* ============================================\n   Footer CTA\n   ============================================ */\n.home-footer {\n  text-align: center;\n}\n\n.home-footer h2 {\n  font-size: 1.75rem;\n  margin: 0 0 1rem 0;\n}\n\n.home-footer p {\n  color: var(--text-muted);\n  margin: 0 0 2rem 0;\n}\n\n.home-footer__links {\n  display: flex;\n  justify-content: center;\n  gap: 2rem;\n}\n\n.home-footer__links a {\n  color: var(--text-muted);\n  text-decoration: none;\n  font-size: 0.9375rem;\n}\n\n.home-footer__links a:hover {\n  color: var(--accent);\n}\n\n/* ============================================\n   Responsive\n   ============================================ */\n@media (max-width: 768px) {\n  .home section {\n    padding: 3.5rem 1.5rem;\n  }\n\n  .hero h1 {\n    font-size: 2.25rem;\n  }\n\n  .pillars {\n    grid-template-columns: 1fr;\n    gap: 1.5rem;\n  }\n\n  .workers__grid {\n    grid-template-columns: 1fr;\n  }\n\n  .quick-links__grid {\n    grid-template-columns: 1fr 1fr;\n  }\n}\n\n@media (max-width: 480px) {\n  .hero h1 {\n    font-size: 1.875rem;\n  }\n\n  .hero__buttons {\n    flex-direction: column;\n  }\n\n  .btn {\n    width: 100%;\n    justify-content: center;\n  }\n\n  .quick-links__grid {\n    grid-template-columns: 1fr;\n  }\n}\n"
  },
  {
    "path": "docs/content/community.md",
    "content": "# Community\n\nConnect with the project through these channels.\n\n## Project management & discussions\n\nProject maintenance guidelines live on the\n[wiki](https://github.com/benoitc/gunicorn/wiki/Project-management).\n\nGitHub is used for:\n\n- [Bug reports](https://github.com/benoitc/gunicorn/issues) — search before\n  opening a new issue.\n- [Discussions](https://github.com/benoitc/gunicorn/discussions) — Q&A and usage\n  tips.\n- [Feature planning](https://github.com/benoitc/gunicorn/issues) — development\n  and project management topics.\n\n## IRC\n\nJoin the Gunicorn channel on [Libera Chat](https://libera.chat/) at\n[`#gunicorn`](https://web.libera.chat/?channels=#gunicorn).\n\n## Issue tracking\n\nFile bugs, enhancements, and tasks in the\n[GitHub issue tracker](https://github.com/benoitc/gunicorn/issues).\n\n## Security issues\n\nReport security vulnerabilities privately to\n[`security@gunicorn.org`](mailto:security@gunicorn.org); only core developers\nsubscribe to this list.\n\n## Contributing\n\nStart with the\n[contributing guide](https://github.com/benoitc/gunicorn/blob/master/CONTRIBUTING.md)\nfor development workflow, code style, and review expectations. New contributors\nare welcome—open a draft pull request early to gather feedback.\n"
  },
  {
    "path": "docs/content/configure.md",
    "content": "<span id=\"configuration\"></span>\n# Configuration Overview\n\nGunicorn reads configuration from five places, in increasing order of priority:\n\n1. Environment variables, for settings that support them.\n2. Framework-specific configuration (currently Paste Deploy only).\n3. A Python configuration file `gunicorn.conf.py` (default in the working directory).\n4. The `GUNICORN_CMD_ARGS` environment variable.\n5. Command-line arguments.\n\nIf a configuration file is provided both via `GUNICORN_CMD_ARGS` and the CLI,\nonly the file specified on the command line is used.\n\n!!! note\n    Print the fully resolved configuration:\n\n    ```bash\n    gunicorn --print-config APP_MODULE\n    ```\n\n    Validate configuration and exit:\n\n    ```bash\n    gunicorn --check-config APP_MODULE\n    ```\n\n    This is also a quick way to confirm that your application can start.\n\n## Command line\n\nOptions set on the command line override framework settings and values from the\nconfiguration file. Not every setting has a command-line flag; run\n\n```bash\ngunicorn -h\n```\n\nfor the complete list. The CLI also exposes `--version`, which is not part of\nthe main [settings reference](reference/settings.md).\n\n<span id=\"configuration_file\"></span>\n## Configuration file\n\nProvide a Python file (for example `gunicorn.conf.py`). Gunicorn executes the\nfile on every start or reload, so any valid Python is allowed:\n\n```python\nimport multiprocessing\n\nbind = \"127.0.0.1:8000\"\nworkers = multiprocessing.cpu_count() * 2 + 1\n```\n\nEvery configuration key is documented in the [settings reference](reference/settings.md).\n\n## Framework settings\n\nAt present only Paste Deploy applications expose framework-specific settings.\nIf you have ideas for Django or other frameworks, open an\n[issue](https://github.com/benoitc/gunicorn/issues).\n\n### Paste applications\n\nReference Gunicorn as the server in your INI file:\n\n```ini\n[server:main]\nuse = egg:gunicorn#main\nhost = 192.168.0.1\nport = 80\nworkers = 2\nproc_name = brim\n```\n\nGunicorn merges any recognised parameters into the base configuration. Values\nfrom the configuration file and command line still override these defaults.\n"
  },
  {
    "path": "docs/content/custom.md",
    "content": "<span id=\"custom\"></span>\n# Custom Application\n\n!!! info \"Added in 19.0\"\n    Use Gunicorn as part of your own WSGI application by subclassing\n    `gunicorn.app.base.BaseApplication`.\n\n\n\nExample: create a tiny WSGI app and load it with a custom application:\n\n```text\n--8<-- \"examples/standalone_app.py\"\n```\n\n\n\n## Using server hooks\n\nProvide hooks through configuration, just like a standard Gunicorn deployment.\nFor example, a `pre_fork` hook:\n\n```python\ndef pre_fork(server, worker):\n    print(f\"pre-fork server {server} worker {worker}\", file=sys.stderr)\n\nif __name__ == \"__main__\":\n    options = {\n        \"bind\": \"127.0.0.1:8080\",\n        \"workers\": number_of_workers(),\n        \"pre_fork\": pre_fork,\n    }\n```\n\n## Direct usage of existing WSGI apps\n\nRun Gunicorn from Python to serve a WSGI application instance at runtime—useful\nfor rolling deploys or packaging with PEX. Gunicorn exposes\n`gunicorn.app.wsgiapp`, which accepts any WSGI app (for example a Flask or\nDjango instance). Assuming your package is `exampleapi` and the application is\n`app`:\n\n```bash\npython -m gunicorn.app.wsgiapp exampleapi:app\n```\n\nAll CLI flags and configuration files still apply:\n\n```bash\n# Custom parameters\npython -m gunicorn.app.wsgiapp exampleapi:app --bind=0.0.0.0:8081 --workers=4\n# Using a config file\npython -m gunicorn.app.wsgiapp exampleapi:app -c config.py\n```\n\nFor PEX builds use `-c gunicorn` at build time so the packaged app accepts the\nentry point at runtime:\n\n```bash\npex . -v -c gunicorn -o compiledapp.pex\n./compiledapp.pex exampleapi:app -c gunicorn_config.py\n```\n"
  },
  {
    "path": "docs/content/deploy.md",
    "content": "# Deploying Gunicorn\n\nWe strongly recommend running Gunicorn behind a proxy server.\n\n## Nginx configuration\n\nAlthough many HTTP proxies exist, we recommend [Nginx](https://nginx.org/).\nWhen using the default synchronous workers you must ensure the proxy buffers\nslow clients; otherwise Gunicorn becomes vulnerable to denial-of-service\nattacks. Use [Hey](https://github.com/rakyll/hey) to verify proxy behaviour.\n\nAn example configuration for fast clients with Nginx\n([source](https://github.com/benoitc/gunicorn/blob/master/examples/nginx.conf)):\n\n```nginx title=\"nginx.conf\"\n--8<-- \"examples/nginx.conf\"\n```\n\n\n\nTo support streaming requests/responses or patterns such as Comet, long\npolling, or WebSockets, disable proxy buffering and run Gunicorn with an async\nworker class:\n\n```nginx\nlocation @proxy_to_app {\n    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    proxy_set_header Host $http_host;\n    proxy_redirect off;\n    proxy_buffering off;\n\n    proxy_pass http://app_server;\n}\n```\n\nTo ignore aborted requests (for example, health checks that close connections\nprematurely) enable\n[`proxy_ignore_client_abort`](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_ignore_client_abort):\n\n```nginx\nproxy_ignore_client_abort on;\n```\n\n!!! note\n    The default value for `proxy_ignore_client_abort` is `off`. If it remains off\n    Nginx logs will report error 499 and Gunicorn may log `Ignoring EPIPE` when the\n    log level is `debug`.\n\n\n\nPass protocol information to Gunicorn so applications can generate correct\nURLs. Add this header to your `location` block:\n\n```nginx\nproxy_set_header X-Forwarded-Proto $scheme;\n```\n\nIf Nginx runs on a different host, tell Gunicorn which proxies are trusted so it\naccepts the `X-Forwarded-*` headers:\n\n```bash\ngunicorn -w 3 --forwarded-allow-ips=\"10.170.3.217,10.170.3.220\" test:app\n```\n\nWhen all traffic comes from trusted proxies (for example Heroku) you can set\n`--forwarded-allow-ips='*'`. This is **dangerous** if untrusted clients can\nreach Gunicorn directly, because forged headers could make your application\nserve secure content over plain HTTP.\n\nGunicorn 19 changed the handling of `REMOTE_ADDR` to conform to\n[RFC 3875](https://www.rfc-editor.org/rfc/rfc3875), meaning it now records the\nproxy IP rather than the upstream client. To log the real client address, set\n[`access_log_format`](reference/settings.md#access_log_format) to include `X-Forwarded-For`:\n\n```text\n%({x-forwarded-for}i)s %(l.md)s %(u.md)s %(t.md)s \"%(r.md)s\" %(s.md)s %(b.md)s \"%(f.md)s\" \"%(a.md)s\"\n```\n\nWhen binding Gunicorn to a UNIX socket `REMOTE_ADDR` will be empty.\n\n## PROXY Protocol\n\nThe [PROXY protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt)\nallows load balancers and reverse proxies to pass original client connection\ninformation (IP address, port) to backend servers. This is especially useful\nwhen TLS termination happens at the proxy layer.\n\nGunicorn supports both PROXY protocol v1 (text format) and v2 (binary format).\n\n### Configuration\n\nEnable PROXY protocol with the `--proxy-protocol` option:\n\n```bash\n# Auto-detect v1 or v2 (recommended)\ngunicorn --proxy-protocol auto app:app\n\n# Force v1 only (text format)\ngunicorn --proxy-protocol v1 app:app\n\n# Force v2 only (binary format, more efficient)\ngunicorn --proxy-protocol v2 app:app\n```\n\nUsing `--proxy-protocol` without a value is equivalent to `auto`.\n\n!!! warning \"Security\"\n    Only enable PROXY protocol when Gunicorn is behind a trusted proxy that sends\n    PROXY headers. Configure [`--proxy-allow-from`](reference/settings.md#proxy_allow_ips)\n    to restrict which IPs can send PROXY protocol headers.\n\n### HAProxy\n\nHAProxy can send PROXY protocol headers to backends. Example configuration:\n\n```haproxy\nfrontend https_front\n    bind *:443 ssl crt /etc/ssl/certs/site.pem\n    default_backend gunicorn_back\n\nbackend gunicorn_back\n    # Send PROXY protocol v2 (binary, more efficient)\n    server gunicorn 127.0.0.1:8000 send-proxy-v2\n\n    # Or use v1 (text format)\n    # server gunicorn 127.0.0.1:8000 send-proxy\n```\n\nStart Gunicorn to accept PROXY protocol:\n\n```bash\ngunicorn -b 127.0.0.1:8000 --proxy-protocol v2 --proxy-allow-from 127.0.0.1 app:app\n```\n\n### stunnel\n\n[stunnel](https://www.stunnel.org/) can terminate TLS and forward connections\nwith PROXY protocol headers:\n\n```ini\n# /etc/stunnel/stunnel.conf\n[https]\naccept = 443\nconnect = 127.0.0.1:8000\ncert = /etc/ssl/certs/stunnel.pem\nkey = /etc/ssl/certs/stunnel.key\nprotocol = proxy\n```\n\nThe `protocol = proxy` directive tells stunnel to prepend PROXY protocol v1\nheaders to forwarded connections.\n\n### AWS/ELB\n\nAWS Network Load Balancers (NLB) and Application Load Balancers (ALB) support\nPROXY protocol v2. Enable it in the target group settings, then configure\nGunicorn:\n\n```bash\ngunicorn --proxy-protocol v2 --proxy-allow-from '*' app:app\n```\n\n!!! note\n    When using `--proxy-allow-from '*'` ensure Gunicorn is not directly\n    accessible from the internet—only through the load balancer.\n\n## Using virtual environments\n\nInstall Gunicorn inside your project\n[virtual environment](https://pypi.python.org/pypi/virtualenv) to keep versions\nisolated:\n\n```bash\nmkdir ~/venvs/\nvirtualenv ~/venvs/webapp\nsource ~/venvs/webapp/bin/activate\npip install gunicorn\ndeactivate\n```\n\nForce installation into the active virtual environment with `--ignore-installed`:\n\n```bash\nsource ~/venvs/webapp/bin/activate\npip install -I gunicorn\n```\n\n## Monitoring\n\n!!! note\n    Do not enable Gunicorn's daemon mode when using process monitors. These\n    supervisors expect to manage the direct child process.\n\n\n\n### Gaffer\n\nUse [Gaffer](https://gaffer.readthedocs.io/) with *gafferd* to manage Gunicorn:\n\n```ini\n[process:gunicorn]\ncmd = gunicorn -w 3 test:app\ncwd = /path/to/project\n```\n\nCreate a `Procfile` if you prefer:\n\n```procfile\ngunicorn = gunicorn -w 3 test:app\n```\n\nStart Gunicorn via Gaffer:\n\n```bash\ngaffer start\n```\n\nOr load it into a running *gafferd* instance:\n\n```bash\ngaffer load\n```\n\n### runit\n\n[runit](http://smarden.org/runit/) is a popular supervisor. A sample service\nscript (see the\n[full example](https://github.com/benoitc/gunicorn/blob/master/examples/gunicorn_rc)):\n\n```bash\n#!/bin/sh\n\nGUNICORN=/usr/local/bin/gunicorn\nROOT=/path/to/project\nPID=/var/run/gunicorn.pid\n\nAPP=main:application\n\nif [ -f $PID ]; then rm $PID; fi\n\ncd $ROOT\nexec $GUNICORN -c $ROOT/gunicorn.conf.py --pid=$PID $APP\n```\n\nSave as `/etc/sv/<app_name>/run`, make it executable, and symlink into\n`/etc/service/<app_name>`. runit will then supervise Gunicorn.\n\n### Supervisor\n\n[Supervisor](http://supervisord.org/) configuration example (adapted from\n[examples/supervisor.conf](https://github.com/benoitc/gunicorn/blob/master/examples/supervisor.conf)):\n\n```ini\n[program:gunicorn]\ncommand=/path/to/gunicorn main:application -c /path/to/gunicorn.conf.py\ndirectory=/path/to/project\nuser=nobody\nautostart=true\nautorestart=true\nredirect_stderr=true\n```\n\n### Upstart\n\nSample Upstart config (logs go to `/var/log/upstart/myapp.log`):\n\n```upstart\n# /etc/init/myapp.conf\n\ndescription \"myapp\"\n\nstart on (filesystem.md)\nstop on runlevel [016]\n\nrespawn\nsetuid nobody\nsetgid nogroup\nchdir /path/to/app/directory\n\nexec /path/to/virtualenv/bin/gunicorn myapp:app\n```\n\n### systemd\n\n[systemd](https://www.freedesktop.org/wiki/Software/systemd/) can create a UNIX\nsocket and launch Gunicorn on demand.\n\nService file:\n\n```ini\n# /etc/systemd/system/gunicorn.service\n\n[Unit]\nDescription=gunicorn daemon\nRequires=gunicorn.socket\nAfter=network.target\n\n[Service]\nType=notify\nNotifyAccess=main\nUser=someuser\nGroup=someuser\nWorkingDirectory=/home/someuser/applicationroot\nExecStart=/usr/bin/gunicorn applicationname.wsgi\nExecReload=/bin/kill -s HUP $MAINPID\nKillMode=mixed\nTimeoutStopSec=5\nPrivateTmp=true\n\n[Install]\nWantedBy=multi-user.target\n```\n\n`Type=notify` lets Gunicorn report readiness to systemd. If the service should\nrun under a transient user consider adding `DynamicUser=true`. Tighten\npermissions further with `ProtectSystem=strict` if the app permits.\n\nSocket activation file:\n\n```ini\n# /etc/systemd/system/gunicorn.socket\n\n[Unit]\nDescription=gunicorn socket\n\n[Socket]\nListenStream=/run/gunicorn.sock\nSocketUser=www-data\nSocketGroup=www-data\nSocketMode=0660\n\n[Install]\nWantedBy=sockets.target\n```\n\nEnable and start the socket so it begins listening immediately and on reboot:\n\n```bash\nsystemctl enable --now gunicorn.socket\n```\n\nTest connectivity from the nginx user (Debian defaults to `www-data`):\n\n```bash\nsudo -u www-data curl --unix-socket /run/gunicorn.sock http\n```\n\n!!! note\n    Use `systemctl show --value -p MainPID gunicorn.service` to retrieve the main\n    process ID or `systemctl kill -s HUP gunicorn.service` to send signals.\n\n\n\nConfigure Nginx to proxy to the new socket:\n\n```nginx\nuser www-data;\n...\nhttp {\n    server {\n        listen          8000;\n        server_name     127.0.0.1;\n        location / {\n            proxy_pass http://unix:/run/gunicorn.sock;\n        }\n    }\n}\n...\n```\n\n!!! note\n    Adjust `listen` and `server_name` for production (typically port 80 and your\n    site's domain).\n\n\n\nEnsure nginx starts automatically:\n\n```bash\nsystemctl enable nginx.service\nsystemctl start nginx\n```\n\nBrowse to <http://127.0.0.1:8000/> to verify Gunicorn + Nginx + systemd.\n\n## Logging\n\nConfigure logging through the CLI flags described in the\n[settings documentation](reference/settings.md#logging) or via a\n[logging configuration file](https://github.com/benoitc/gunicorn/blob/master/examples/logging.conf).\nRotate logs with `logrotate` by sending `SIGUSR1`:\n\n```bash\nkill -USR1 $(cat /var/run/gunicorn.pid)\n```\n\n!!! note\n    If you override the `LOGGING` dictionary, set `disable_existing_loggers` to\n    `False` so Gunicorn's loggers remain active.\n\n\n\n!!! warning\n    Gunicorn's error log should capture Gunicorn-related messages only. Route your\n    application logs separately.\n\n\n"
  },
  {
    "path": "docs/content/design.md",
    "content": "<span id=\"design\"></span>\n# Design\n\nA brief look at Gunicorn's architecture.\n\n## Server Model\n\nGunicorn uses a **pre-fork worker model**: an arbiter process manages worker\nprocesses, while the workers handle requests and responses. The arbiter never\ntouches individual client sockets.\n\n<div class=\"pillars\" markdown>\n\n<div class=\"pillar\" markdown>\n<div class=\"pillar__icon\">⚖️</div>\n\n### Arbiter\n\nOrchestrates the worker pool. Listens for signals (`TTIN`, `TTOU`, `CHLD`,\n`HUP`) to adjust workers, restart them on failure, or reload configuration.\n</div>\n\n<div class=\"pillar\" markdown>\n<div class=\"pillar__icon\">⚙️</div>\n\n### Worker Pool\n\nEach worker handles requests independently. Worker types determine\nconcurrency model: sync, threaded, or async via greenlets/asyncio.\n</div>\n\n<div class=\"pillar\" markdown>\n<div class=\"pillar__icon\">📡</div>\n\n### Signal Communication\n\n`TTIN`/`TTOU` adjust worker count. `CHLD` triggers restart of crashed\nworkers. `HUP` reloads configuration. See [Signals](signals.md).\n</div>\n\n</div>\n\n## Worker Types\n\nChoose a worker type based on your application's needs.\n\n=== \"Sync\"\n\n    The **default** worker. Handles one request at a time per worker.\n\n    - Simple and predictable\n    - Errors affect only the current request\n    - No keep-alive support (connections close after response)\n    - Requires a buffering proxy (nginx, HAProxy) for production\n\n    ```bash\n    gunicorn myapp:app\n    ```\n\n=== \"Gthread\"\n\n    Threaded worker with a **thread pool** per worker process.\n\n    - Supports keep-alive connections\n    - Good balance of concurrency and simplicity\n    - Threads share memory (lower footprint than workers)\n    - Idle connections close after keepalive timeout\n\n    ```bash\n    gunicorn myapp:app -k gthread --threads 4\n    ```\n\n=== \"ASGI\"\n\n    Native **asyncio** support for modern async frameworks.\n\n    - For FastAPI, Starlette, Quart, and other ASGI apps\n    - Full async/await support\n    - See the [ASGI Guide](asgi.md) for details\n\n    ```bash\n    gunicorn myapp:app -k uvicorn.workers.UvicornWorker\n    ```\n\n=== \"Gevent\"\n\n    **Greenlet-based** async worker using [Gevent](http://www.gevent.org/).\n\n    - Handles thousands of concurrent connections\n    - Supports keep-alive, WebSockets, long-polling\n    - May require patches for some libraries (e.g., `psycogreen` for Psycopg)\n    - Not compatible with code that relies on blocking behavior\n\n    ```bash\n    gunicorn myapp:app -k gevent --worker-connections 1000\n    ```\n\n=== \"Eventlet (Deprecated)\"\n\n    !!! warning \"Deprecated\"\n        The eventlet worker is **deprecated** and will be removed in Gunicorn 26.0.\n        Eventlet itself is [no longer actively maintained](https://eventlet.readthedocs.io/en/latest/asyncio/migration.html).\n        Please migrate to `gevent`, `gthread`, or another supported worker type.\n\n    **Greenlet-based** async worker using [Eventlet](http://eventlet.net/).\n\n    - Similar capabilities to Gevent\n    - Handles high concurrency for I/O-bound apps\n    - Some libraries may need compatibility patches\n\n    ```bash\n    gunicorn myapp:app -k eventlet --worker-connections 1000\n    ```\n\n=== \"Tornado\"\n\n    Worker for [Tornado](https://www.tornadoweb.org/) applications.\n\n    - Designed for Tornado's async framework\n    - Can serve WSGI apps, but not recommended for that use case\n    - Use when running native Tornado applications\n\n    ```bash\n    gunicorn myapp:app -k tornado\n    ```\n\n## Comparison\n\n| Worker | Concurrency Model | Keep-Alive | Best For |\n|--------|-------------------|------------|----------|\n| `sync` | 1 request/worker | ❌ | CPU-bound apps behind a proxy |\n| `gthread` | Thread pool | ✅ | Mixed workloads, moderate concurrency |\n| ASGI workers | AsyncIO | ✅ | Modern async frameworks (FastAPI, etc.) |\n| `gevent` | Greenlets | ✅ | I/O-bound, WebSockets, streaming |\n| `eventlet` | Greenlets | ✅ | **Deprecated** - use `gevent` instead |\n| `tornado` | Tornado IOLoop | ✅ | Native Tornado applications |\n\n!!! tip \"Quick Decision Guide\"\n\n    - **Simple app behind nginx?** → `sync` (default)\n    - **Need keep-alive or moderate concurrency?** → `gthread`\n    - **WebSockets, streaming, long-polling?** → `gevent` or ASGI worker\n    - **FastAPI, Starlette, or async framework?** → ASGI worker\n\n## When to Use Async Workers\n\nSynchronous workers assume your app is CPU or network bound and avoids\nindefinite blocking operations. Use async workers when you have:\n\n- Long blocking calls (external APIs, slow databases)\n- Direct internet traffic without a buffering proxy\n- Streaming request/response bodies\n- Long polling or Comet patterns\n- WebSockets\n\n!!! info \"Testing Slow Clients\"\n\n    Tools like [Hey](https://github.com/rakyll/hey) can simulate slow responses\n    to test how your configuration handles them.\n\n## Scaling\n\n### How Many Workers?\n\n!!! warning \"Don't Over-Scale\"\n\n    Workers ≠ clients. Gunicorn typically needs only **4–12 workers** to handle\n    heavy traffic. Too many workers waste resources and can reduce throughput.\n\nStart with this formula and adjust under load:\n\n```\nworkers = (2 × CPU cores) + 1\n```\n\nUse `TTIN`/`TTOU` signals to adjust the worker count at runtime.\n\n### How Many Threads?\n\nWith the `gthread` worker, you can combine workers and threads:\n\n```bash\ngunicorn myapp:app -k gthread --workers 4 --threads 2\n```\n\n!!! info \"Threads vs Workers\"\n\n    - **Threads** share memory → lower footprint\n    - **Workers** isolate failures → better fault tolerance\n    - Combine both for the best of both worlds\n\nThreads can extend request time beyond the worker timeout while still\nnotifying the arbiter. The optimal mix depends on your runtime (CPython vs\nPyPy) and workload.\n\n## Configuration Examples\n\n```bash\n# Sync (default) - simple apps behind nginx\ngunicorn myapp:app\n\n# Gthread - keep-alive and thread concurrency\ngunicorn myapp:app -k gthread --workers 4 --threads 4\n\n# Gevent - high concurrency for I/O-bound apps\ngunicorn myapp:app -k gevent --workers 4 --worker-connections 1000\n\n# ASGI - FastAPI/Starlette with Uvicorn worker\ngunicorn myapp:app -k uvicorn.workers.UvicornWorker --workers 4\n```\n\n<span id=\"asyncio-workers\"></span>\n\n!!! note \"Third-Party AsyncIO Workers\"\n\n    For asyncio frameworks, you can also use third-party workers. See the\n    [aiohttp deployment guide](https://docs.aiohttp.org/en/stable/deployment.html#nginx-gunicorn)\n    for examples.\n"
  },
  {
    "path": "docs/content/dirty.md",
    "content": "---\ntitle: Dirty Arbiters\nmenu:\n    guides:\n        weight: 10\n---\n\n# Dirty Arbiters\n\n!!! warning \"Beta Feature\"\n    Dirty Arbiters is a beta feature introduced in Gunicorn 25.0.0. While it has been tested,\n    the API and behavior may change in future releases. Please report any issues on\n    [GitHub](https://github.com/benoitc/gunicorn/issues).\n\nDirty Arbiters provide a separate process pool for executing long-running, blocking operations (AI model loading, heavy computation) without blocking HTTP workers. This feature is inspired by Erlang's dirty schedulers.\n\n## Overview\n\nTraditional Gunicorn workers are designed to handle HTTP requests quickly. Long-running operations like loading ML models or performing heavy computation can block these workers, reducing the server's ability to handle concurrent requests.\n\nDirty Arbiters solve this by providing:\n\n- **Separate worker pool** - Completely separate from HTTP workers, can be killed/restarted independently\n- **Stateful workers** - Loaded resources persist in dirty worker memory\n- **Message-passing IPC** - Communication via Unix sockets with binary TLV protocol\n- **Explicit API** - Clear `execute()` calls (no hidden IPC)\n- **Asyncio-based** - Clean concurrent handling with streaming support\n\n## Design Philosophy\n\nDirty Arbiters follow several key design principles:\n\n### Separate Process Hierarchy\n\nUnlike threads or in-process pools, Dirty Arbiters use a fully separate process tree:\n\n- **Isolation** - A crash or memory leak in a dirty worker cannot affect HTTP workers\n- **Independent lifecycle** - Dirty workers can be killed/restarted without affecting request handling\n- **Resource accounting** - OS-level memory limits can be applied per-process\n- **Clean shutdown** - Each process tree can be signaled and terminated independently\n\n### Erlang Inspiration\n\nThe name and concept come from Erlang's \"dirty schedulers\" - special schedulers that handle operations that would block normal schedulers. In Erlang, dirty schedulers run NIFs (Native Implemented Functions) that can't yield. Similarly, Gunicorn's Dirty Arbiters handle Python operations that would block HTTP workers.\n\n### Why Asyncio\n\nThe Dirty Arbiter uses asyncio for its core loop rather than the main arbiter's select-based approach:\n\n- **Non-blocking IPC** - Can handle many concurrent client connections efficiently\n- **Concurrent request routing** - Multiple requests can be dispatched to workers simultaneously\n- **Streaming support** - Native async generators for streaming responses\n- **Clean signal handling** - Signals integrate cleanly via `loop.add_signal_handler()`\n\n### Stateful Applications\n\nTraditional WSGI apps are request-scoped - they're invoked per-request and don't maintain state between requests. Dirty apps are different:\n\n- **Long-lived** - Apps persist in worker memory for the worker's lifetime\n- **Pre-loaded resources** - Models, connections, and caches stay loaded\n- **Explicit state management** - Apps control their own lifecycle via `init()` and `close()`\n\nThis makes dirty apps ideal for ML inference, where loading a model once and reusing it for many requests is essential.\n\n## Architecture\n\n```\n                         +-------------------+\n                         |   Main Arbiter    |\n                         | (manages both)    |\n                         +--------+----------+\n                                  |\n                    SIGTERM/SIGHUP/SIGUSR1 (forwarded)\n                                  |\n           +----------------------+----------------------+\n           |                                             |\n     +-----v-----+                                +------v------+\n     | HTTP      |                                | Dirty       |\n     | Workers   |                                | Arbiter     |\n     +-----------+                                +------+------+\n           |                                             |\n           |    Unix Socket IPC                   SIGTERM/SIGHUP\n           |    /tmp/gunicorn_dirty_<pid>.sock          |\n           +------------------>---------------------->---+\n                                             +-----------+-----------+\n                                             |           |           |\n                                       +-----v---+ +-----v---+ +-----v---+\n                                       | Dirty   | | Dirty   | | Dirty   |\n                                       | Worker  | | Worker  | | Worker  |\n                                       +---------+ +---------+ +---------+\n                                          ^   |        ^   |       ^   |\n                                          |   |        |   |       |   |\n                                    Heartbeat (mtime every dirty_timeout/2)\n                                          |   |        |   |       |   |\n                                          +---+--------+---+-------+---+\n                                                       |\n                                     Workers load apps based on allocation\n                                     Worker 1: [MLApp, ImageApp, HeavyApp]\n                                     Worker 2: [MLApp, ImageApp, HeavyApp]\n                                     Worker 3: [MLApp, ImageApp]  (HeavyApp workers=2)\n```\n\n### Process Relationships\n\n| Component | Parent | Communication |\n|-----------|--------|---------------|\n| Main Arbiter | init/systemd | Signals from OS |\n| HTTP Workers | Main Arbiter | Pipes, signals |\n| Dirty Arbiter | Main Arbiter | Signals, exit status |\n| Dirty Workers | Dirty Arbiter | Unix socket, signals, WorkerTmp |\n\n## Configuration\n\nAdd these settings to your Gunicorn configuration file or command line:\n\n```python\n# gunicorn.conf.py\ndirty_apps = [\n    \"myapp.ml:MLApp\",\n    \"myapp.images:ImageApp\",\n]\ndirty_workers = 2          # Number of dirty workers\ndirty_timeout = 300        # Task timeout in seconds\ndirty_threads = 1          # Threads per worker\ndirty_graceful_timeout = 30  # Shutdown timeout\n```\n\nOr via command line:\n\n```bash\ngunicorn myapp:app \\\n    --dirty-app myapp.ml:MLApp \\\n    --dirty-app myapp.images:ImageApp \\\n    --dirty-workers 2 \\\n    --dirty-timeout 300\n```\n\n### Configuration Options\n\n| Setting | Default | Description |\n|---------|---------|-------------|\n| `dirty_apps` | `[]` | List of dirty app import paths |\n| `dirty_workers` | `0` | Number of dirty workers (0 = disabled) |\n| `dirty_timeout` | `300` | Task timeout in seconds |\n| `dirty_threads` | `1` | Threads per dirty worker |\n| `dirty_graceful_timeout` | `30` | Graceful shutdown timeout |\n\n## Per-App Worker Allocation\n\nBy default, all dirty workers load all configured apps. For apps that consume significant memory (like large ML models), you can limit how many workers load a specific app.\n\n### Why Per-App Allocation?\n\nConsider a scenario with a 10GB ML model and 8 dirty workers:\n\n- **Default behavior**: 8 workers × 10GB = 80GB RAM\n- **With `workers=2`**: 2 workers × 10GB = 20GB RAM (75% savings)\n\nRequests for the limited app are routed only to workers that have it loaded.\n\n### Configuration Methods\n\n**Method 1: Class Attribute**\n\nSet the `workers` attribute on your DirtyApp class:\n\n```python\nfrom gunicorn.dirty import DirtyApp\n\nclass HeavyModelApp(DirtyApp):\n    workers = 2  # Only 2 workers will load this app\n\n    def init(self):\n        self.model = load_10gb_model()\n\n    def predict(self, data):\n        return self.model.predict(data)\n\n    def close(self):\n        pass\n```\n\n**Method 2: Config Override**\n\nUse the `module:class:N` format in your config:\n\n```python\n# gunicorn.conf.py\ndirty_apps = [\n    \"myapp.light:LightApp\",           # All workers (default)\n    \"myapp.heavy:HeavyModelApp:2\",    # Only 2 workers\n    \"myapp.single:SingletonApp:1\",    # Only 1 worker\n]\ndirty_workers = 4\n```\n\nConfig overrides take precedence over class attributes.\n\n### Worker Distribution\n\nWhen workers spawn, apps are assigned based on their limits:\n\n```\nExample with dirty_workers=4:\n  - LightApp (workers=None):  Loaded on workers 1, 2, 3, 4\n  - HeavyModelApp (workers=2): Loaded on workers 1, 2\n  - SingletonApp (workers=1):  Loaded on worker 1\n\nWorker 1: [LightApp, HeavyModelApp, SingletonApp]\nWorker 2: [LightApp, HeavyModelApp]\nWorker 3: [LightApp]\nWorker 4: [LightApp]\n```\n\n### Request Routing\n\nRequests are automatically routed to workers that have the target app:\n\n```python\nclient = get_dirty_client()\n\n# Goes to any of 4 workers (round-robin)\nclient.execute(\"myapp.light:LightApp\", \"action\")\n\n# Goes to worker 1 or 2 only (round-robin between those)\nclient.execute(\"myapp.heavy:HeavyModelApp\", \"predict\", data)\n\n# Always goes to worker 1\nclient.execute(\"myapp.single:SingletonApp\", \"process\")\n```\n\n### Error Handling\n\nIf no workers have the requested app loaded, a `DirtyNoWorkersAvailableError` is raised:\n\n```python\nfrom gunicorn.dirty import get_dirty_client\nfrom gunicorn.dirty.errors import DirtyNoWorkersAvailableError\n\ndef my_view(request):\n    client = get_dirty_client()\n    try:\n        result = client.execute(\"myapp.heavy:HeavyModelApp\", \"predict\", data)\n    except DirtyNoWorkersAvailableError as e:\n        # All workers with this app are down or app not configured\n        return {\"error\": \"Service temporarily unavailable\", \"app\": e.app_path}\n```\n\n### Worker Crash Recovery\n\nWhen a worker crashes, its replacement gets the **same apps** as the dead worker:\n\n```\nTimeline:\n  t=0: Worker 1 crashes (had HeavyModelApp)\n  t=1: Arbiter detects crash, queues respawn\n  t=2: New Worker 5 spawns with same apps as Worker 1\n  t=3: HeavyModelApp still available on Worker 2 during gap\n```\n\nThis ensures:\n\n- No memory redistribution on existing workers\n- Predictable replacement behavior\n- The heavy model is only loaded on the new worker\n\n### Best Practices\n\n1. **Set realistic limits** - Don't set `workers=1` unless truly necessary (single point of failure)\n2. **Monitor memory** - Track per-worker memory to tune allocation\n3. **Handle unavailability** - Catch `DirtyNoWorkersAvailableError` gracefully\n4. **Use class attributes for app-specific limits** - Makes the limit part of the app definition\n5. **Use config for deployment-specific overrides** - Different limits for dev vs prod\n\n## Creating a Dirty App\n\nDirty apps inherit from `DirtyApp` and implement three methods:\n\n```python\n# myapp/dirty.py\nfrom gunicorn.dirty import DirtyApp\n\nclass MLApp(DirtyApp):\n    \"\"\"Dirty application for ML workloads.\"\"\"\n\n    def __init__(self):\n        self.models = {}\n\n    def init(self):\n        \"\"\"Called once at dirty worker startup.\"\"\"\n        # Pre-load commonly used models\n        self.models['default'] = self._load_model('base-model')\n\n    def __call__(self, action, *args, **kwargs):\n        \"\"\"Dispatch to action methods.\"\"\"\n        method = getattr(self, action, None)\n        if method is None:\n            raise ValueError(f\"Unknown action: {action}\")\n        return method(*args, **kwargs)\n\n    def load_model(self, name):\n        \"\"\"Load a model into memory.\"\"\"\n        if name not in self.models:\n            self.models[name] = self._load_model(name)\n        return {\"loaded\": True, \"name\": name}\n\n    def inference(self, model_name, input_text):\n        \"\"\"Run inference on loaded model.\"\"\"\n        model = self.models.get(model_name)\n        if not model:\n            raise ValueError(f\"Model not loaded: {model_name}\")\n        return model.predict(input_text)\n\n    def _load_model(self, name):\n        import torch\n        model = torch.load(f\"models/{name}.pt\")\n        return model\n\n    def close(self):\n        \"\"\"Cleanup on shutdown.\"\"\"\n        for model in self.models.values():\n            del model\n```\n\n### DirtyApp Interface\n\n| Method/Attribute | Description |\n|------------------|-------------|\n| `workers` | Class attribute. Number of workers to load this app (`None` = all workers). |\n| `init()` | Called once when dirty worker starts, after instantiation. Load resources here. |\n| `__call__(action, *args, **kwargs)` | Handle requests from HTTP workers. |\n| `close()` | Called when dirty worker shuts down. Cleanup resources. |\n\n### Initialization Sequence\n\nWhen a dirty worker starts, initialization happens in this order:\n\n1. **Fork** - Worker process is forked from dirty arbiter\n2. **`dirty_post_fork(arbiter, worker)`** - Hook called immediately after fork\n3. **App instantiation** - Each dirty app class is instantiated (`__init__`)\n4. **`app.init()`** - Called for each app after instantiation (load models, resources)\n5. **`dirty_worker_init(worker)`** - Hook called after ALL apps are initialized\n6. **Run loop** - Worker starts accepting requests from HTTP workers\n\nThis means:\n\n- Use `__init__` for basic setup (initialize empty containers, store config)\n- Use `init()` for heavy loading (ML models, database connections, large files)\n- The `dirty_worker_init` hook fires only after all apps have completed their `init()` calls\n\n## Using from HTTP Workers\n\n### Sync Workers (sync, gthread)\n\n```python\nfrom gunicorn.dirty import get_dirty_client\n\ndef my_view(request):\n    client = get_dirty_client()\n\n    # Load a model\n    client.execute(\"myapp.ml:MLApp\", \"load_model\", \"gpt-4\")\n\n    # Run inference\n    result = client.execute(\n        \"myapp.ml:MLApp\",\n        \"inference\",\n        \"gpt-4\",\n        input_text=request.data\n    )\n    return result\n```\n\n### Async Workers (ASGI)\n\n```python\nfrom gunicorn.dirty import get_dirty_client_async\n\nasync def my_view(request):\n    client = await get_dirty_client_async()\n\n    # Non-blocking execution\n    await client.execute_async(\"myapp.ml:MLApp\", \"load_model\", \"gpt-4\")\n\n    result = await client.execute_async(\n        \"myapp.ml:MLApp\",\n        \"inference\",\n        \"gpt-4\",\n        input_text=request.data\n    )\n    return result\n```\n\n## Streaming\n\nDirty Arbiters support streaming responses for use cases like LLM token generation, where data is produced incrementally. This enables real-time delivery of results without waiting for complete execution.\n\n### Streaming with Generators\n\nAny dirty app action that returns a generator (sync or async) automatically streams chunks to the client:\n\n```python\n# myapp/llm.py\nfrom gunicorn.dirty import DirtyApp\n\nclass LLMApp(DirtyApp):\n    def init(self):\n        from transformers import pipeline\n        self.generator = pipeline(\"text-generation\", model=\"gpt2\")\n\n    def generate(self, prompt):\n        \"\"\"Sync streaming - yields tokens.\"\"\"\n        for token in self.generator(prompt, stream=True):\n            yield token[\"generated_text\"]\n\n    async def generate_async(self, prompt):\n        \"\"\"Async streaming - yields tokens.\"\"\"\n        import openai\n        client = openai.AsyncOpenAI()\n        stream = await client.chat.completions.create(\n            model=\"gpt-4\",\n            messages=[{\"role\": \"user\", \"content\": prompt}],\n            stream=True\n        )\n        async for chunk in stream:\n            if chunk.choices[0].delta.content:\n                yield chunk.choices[0].delta.content\n\n    def close(self):\n        pass\n```\n\n### Client Streaming API\n\nUse `stream()` for sync workers and `stream_async()` for async workers:\n\n**Sync Workers (sync, gthread):**\n\n```python\nfrom gunicorn.dirty import get_dirty_client\n\ndef generate_view(request):\n    client = get_dirty_client()\n\n    def generate_response():\n        for chunk in client.stream(\"myapp.llm:LLMApp\", \"generate\", request.prompt):\n            yield chunk\n\n    return StreamingResponse(generate_response())\n```\n\n**Async Workers (ASGI):**\n\n```python\nfrom gunicorn.dirty import get_dirty_client_async\n\nasync def generate_view(request):\n    client = await get_dirty_client_async()\n\n    async def generate_response():\n        async for chunk in client.stream_async(\"myapp.llm:LLMApp\", \"generate\", request.prompt):\n            yield chunk\n\n    return StreamingResponse(generate_response())\n```\n\n### Streaming Protocol\n\nStreaming uses a simple protocol with three message types:\n\n1. **Chunk** (`type: \"chunk\"`) - Contains partial data\n2. **End** (`type: \"end\"`) - Signals stream completion\n3. **Error** (`type: \"error\"`) - Signals error during streaming\n\nExample message flow:\n```\nClient -> Arbiter -> Worker: request\nWorker -> Arbiter -> Client: chunk (data: \"Hello\")\nWorker -> Arbiter -> Client: chunk (data: \" \")\nWorker -> Arbiter -> Client: chunk (data: \"World\")\nWorker -> Arbiter -> Client: end\n```\n\n## Binary Protocol\n\nThe dirty worker IPC uses a binary protocol inspired by OpenBSD msgctl/msgsnd for efficient data transfer. This eliminates base64 encoding overhead for binary data like images, audio, or model weights.\n\n### Header Format (16 bytes)\n\n```\n+--------+--------+--------+--------+--------+--------+--------+--------+\n|  Magic (2B)     | Ver(1) | MType  |        Payload Length (4B)        |\n+--------+--------+--------+--------+--------+--------+--------+--------+\n|                       Request ID (8 bytes)                            |\n+--------+--------+--------+--------+--------+--------+--------+--------+\n```\n\n- **Magic**: `0x47 0x44` (\"GD\" for Gunicorn Dirty)\n- **Version**: `0x01`\n- **MType**: Message type (`0x01`=REQUEST, `0x02`=RESPONSE, `0x03`=ERROR, `0x04`=CHUNK, `0x05`=END)\n- **Length**: Payload size (big-endian uint32, max 64MB)\n- **Request ID**: uint64 identifier\n\n### TLV Payload Encoding\n\nPayloads use Type-Length-Value encoding:\n\n| Type | Code | Description |\n|------|------|-------------|\n| None | `0x00` | No value bytes |\n| Bool | `0x01` | 1 byte (0x00/0x01) |\n| Int64 | `0x05` | 8 bytes big-endian signed |\n| Float64 | `0x06` | 8 bytes IEEE 754 |\n| Bytes | `0x10` | 4-byte length + raw bytes |\n| String | `0x11` | 4-byte length + UTF-8 |\n| List | `0x20` | 4-byte count + elements |\n| Dict | `0x21` | 4-byte count + key-value pairs |\n\n### Binary Data Benefits\n\nThe binary protocol allows passing raw bytes directly without encoding:\n\n```python\n# Image processing with binary data\ndef resize(self, image_data, width, height):\n    \"\"\"Resize an image - image_data is raw bytes.\"\"\"\n    img = Image.open(io.BytesIO(image_data))\n    resized = img.resize((width, height))\n    buffer = io.BytesIO()\n    resized.save(buffer, format='PNG')\n    return buffer.getvalue()  # Returns raw bytes\n\n# Called from HTTP worker\nthumbnail = client.execute(\n    \"myapp.images:ImageApp\",\n    \"thumbnail\",\n    raw_image_bytes,  # No base64 encoding needed\n    size=256\n)\n```\n\n### Error Handling in Streams\n\nErrors during streaming are delivered as error messages:\n\n```python\ndef generate_view(request):\n    client = get_dirty_client()\n\n    try:\n        for chunk in client.stream(\"myapp.llm:LLMApp\", \"generate\", prompt):\n            yield chunk\n    except DirtyError as e:\n        # Error occurred mid-stream\n        yield f\"\\n[Error: {e.message}]\"\n```\n\n### Best Practices for Streaming\n\n1. **Use async generators for I/O-bound streaming** - e.g., API calls to external services\n2. **Use sync generators for CPU-bound streaming** - e.g., local model inference\n3. **Yield frequently** - Heartbeats are sent during streaming to keep workers alive\n4. **Keep chunks small** - Smaller chunks provide better perceived latency\n5. **Handle client disconnection** - Streams continue even if client disconnects; design accordingly\n\n## Stash (Shared State via Message Passing)\n\nStash provides shared state between dirty workers, similar to Erlang's ETS (Erlang Term Storage). Workers remain fully isolated - all state access goes through message passing to the arbiter.\n\n### Architecture\n\n```\n                    +------------------+\n                    |   Dirty Arbiter  |\n                    |                  |\n                    |  stash_tables:   |\n                    |    sessions: {}  |\n                    |    cache: {}     |\n                    +--------+---------+\n                             |\n              Unix Socket IPC (message passing)\n                             |\n         +-------------------+-------------------+\n         |                   |                   |\n   +-----v-----+       +-----v-----+       +-----v-----+\n   |  Worker 1 |       |  Worker 2 |       |  Worker 3 |\n   |           |       |           |       |           |\n   | (isolated)|       | (isolated)|       | (isolated)|\n   +-----------+       +-----------+       +-----------+\n\n   Workers have NO shared memory.\n   All stash operations are IPC messages to arbiter.\n```\n\n### How It Works\n\n1. Worker calls `stash.put(\"sessions\", \"user:1\", data)`\n2. Worker sends message to arbiter via Unix socket\n3. Arbiter stores data in its memory (`self.stash_tables`)\n4. Arbiter sends response back to worker\n5. Worker receives confirmation\n\nThis is **not** shared memory - workers remain fully isolated. The arbiter acts as a centralized store that workers communicate with via message passing. This matches Erlang's model where ETS tables are owned by a process.\n\n### Basic Usage\n\n```python\nfrom gunicorn.dirty import stash\n\n# Store a value (table auto-created)\n# This sends a message to arbiter, which stores it\nstash.put(\"sessions\", \"user:123\", {\"name\": \"Alice\", \"role\": \"admin\"})\n\n# Retrieve a value\n# This sends a request to arbiter, which returns the value\nuser = stash.get(\"sessions\", \"user:123\")\n\n# Delete a key\nstash.delete(\"sessions\", \"user:123\")\n\n# Check existence\nif stash.exists(\"sessions\", \"user:123\"):\n    print(\"Session exists\")\n\n# List keys with pattern matching\nkeys = stash.keys(\"sessions\", pattern=\"user:*\")\n```\n\n### Dict-like Interface\n\nFor more Pythonic access, use the table interface:\n\n```python\nfrom gunicorn.dirty import stash\n\n# Get a table reference\nsessions = stash.table(\"sessions\")\n\n# Dict-like operations (each is an IPC message)\nsessions[\"user:123\"] = {\"name\": \"Alice\"}\nuser = sessions[\"user:123\"]\ndel sessions[\"user:123\"]\n\n# Iteration\nfor key in sessions:\n    print(key, sessions[key])\n\n# Length\ncount = len(sessions)\n```\n\n### Table Management\n\n```python\nfrom gunicorn.dirty import stash\n\n# Explicit table creation (idempotent)\nstash.ensure(\"cache\")\n\n# Get table info\ninfo = stash.info(\"sessions\")\nprint(f\"Table has {info['size']} entries\")\n\n# Clear all entries in a table\nstash.clear(\"sessions\")\n\n# Delete entire table\nstash.delete_table(\"sessions\")\n\n# List all tables\ntables = stash.tables()\n```\n\n### Using Stash in DirtyApp\n\nDeclare tables your app uses with the `stashes` class attribute:\n\n```python\nfrom gunicorn.dirty import DirtyApp, stash\n\nclass SessionApp(DirtyApp):\n    # Tables declared here are auto-created on startup\n    stashes = [\"sessions\", \"counters\"]\n\n    def init(self):\n        # Initialize counter if needed\n        if not stash.exists(\"counters\", \"requests\"):\n            stash.put(\"counters\", \"requests\", 0)\n\n    def login(self, user_id, user_data):\n        \"\"\"Store session - any worker can read it via arbiter.\"\"\"\n        stash.put(\"sessions\", f\"user:{user_id}\", {\n            \"data\": user_data,\n            \"logged_in_at\": time.time(),\n        })\n        self._increment_counter()\n        return {\"status\": \"ok\"}\n\n    def get_session(self, user_id):\n        \"\"\"Get session - request goes to arbiter.\"\"\"\n        return stash.get(\"sessions\", f\"user:{user_id}\")\n\n    def _increment_counter(self):\n        \"\"\"Increment global counter via arbiter.\"\"\"\n        current = stash.get(\"counters\", \"requests\", 0)\n        stash.put(\"counters\", \"requests\", current + 1)\n\n    def close(self):\n        pass\n```\n\n### API Reference\n\n| Function | Description |\n|----------|-------------|\n| `stash.put(table, key, value)` | Store a value (table auto-created) |\n| `stash.get(table, key, default=None)` | Retrieve a value |\n| `stash.delete(table, key)` | Delete a key, returns True if deleted |\n| `stash.exists(table, key=None)` | Check if table/key exists |\n| `stash.keys(table, pattern=None)` | List keys, optional glob pattern |\n| `stash.clear(table)` | Delete all entries in table |\n| `stash.info(table)` | Get table info (size, etc.) |\n| `stash.ensure(table)` | Create table if not exists |\n| `stash.delete_table(table)` | Delete entire table |\n| `stash.tables()` | List all table names |\n| `stash.table(name)` | Get dict-like interface |\n\n### Patterns and Use Cases\n\n**Session Storage:**\n```python\n# Store session on login (worker 1)\nstash.put(\"sessions\", f\"user:{user_id}\", session_data)\n\n# Check session on request (may be worker 2)\nsession = stash.get(\"sessions\", f\"user:{user_id}\")\nif session is None:\n    raise AuthError(\"Not logged in\")\n```\n\n**Shared Cache:**\n```python\ndef get_expensive_result(key):\n    # Check cache first (via arbiter)\n    cached = stash.get(\"cache\", key)\n    if cached is not None:\n        return cached\n\n    # Compute and cache\n    result = expensive_computation()\n    stash.put(\"cache\", key, result)\n    return result\n```\n\n**Global Counters:**\n```python\ndef increment_counter(name):\n    # Note: not atomic - two workers could read same value\n    current = stash.get(\"counters\", name, 0)\n    stash.put(\"counters\", name, current + 1)\n    return current + 1\n```\n\n**Feature Flags:**\n```python\n# Set flag (from admin endpoint)\nstash.put(\"flags\", \"new_feature\", True)\n\n# Check flag (from any worker)\nif stash.get(\"flags\", \"new_feature\", False):\n    enable_new_feature()\n```\n\n### Error Handling\n\n```python\nfrom gunicorn.dirty.stash import (\n    StashError,\n    StashTableNotFoundError,\n    StashKeyNotFoundError,\n)\n\ntry:\n    info = stash.info(\"nonexistent\")\nexcept StashTableNotFoundError as e:\n    print(f\"Table not found: {e.table_name}\")\n\n# Using get() with default avoids KeyNotFoundError\nvalue = stash.get(\"table\", \"key\", default=\"fallback\")\n```\n\n### Best Practices\n\n1. **Use descriptive table names** - `user_sessions`, `ml_cache`, not `data`\n2. **Use key prefixes** - `user:123`, `cache:model:v1` for organization\n3. **Handle missing data** - Always provide defaults or check existence\n4. **Don't store large data** - Each access is an IPC round-trip\n5. **Remember it's ephemeral** - Data is lost on arbiter restart\n\n### Advantages\n\n- **Worker isolation** - Workers remain fully isolated; no shared memory bugs\n- **Simple API** - Dict-like interface, no locking required\n- **Binary support** - Efficiently stores bytes (images, model weights)\n- **Pattern matching** - `keys(pattern=\"user:*\")` for querying\n- **Zero setup** - Works automatically with dirty workers\n- **Table-based** - Organize data into logical namespaces\n\n### Limitations\n\n- **No persistence** - Data lives only in arbiter memory\n- **No transactions** - No atomic read-modify-write operations\n- **No TTL** - Entries don't expire automatically\n- **IPC overhead** - Each operation is a network round-trip\n- **Single arbiter** - Not distributed across multiple machines\n\nFor persistent or distributed state, use Redis, PostgreSQL, or similar external systems.\n\n### Flask Example\n\n```python\nfrom flask import Flask, Response\nfrom gunicorn.dirty import get_dirty_client\n\napp = Flask(__name__)\n\n@app.route(\"/chat\", methods=[\"POST\"])\ndef chat():\n    prompt = request.json.get(\"prompt\")\n    client = get_dirty_client()\n\n    def stream():\n        for token in client.stream(\"myapp.llm:LLMApp\", \"generate\", prompt):\n            yield f\"data: {token}\\n\\n\"\n\n    return Response(stream(), content_type=\"text/event-stream\")\n```\n\n### FastAPI Example\n\n```python\nfrom fastapi import FastAPI\nfrom fastapi.responses import StreamingResponse\nfrom gunicorn.dirty import get_dirty_client_async\n\napp = FastAPI()\n\n@app.post(\"/chat\")\nasync def chat(prompt: str):\n    client = await get_dirty_client_async()\n\n    async def stream():\n        async for token in client.stream_async(\"myapp.llm:LLMApp\", \"generate\", prompt):\n            yield f\"data: {token}\\n\\n\"\n\n    return StreamingResponse(stream(), media_type=\"text/event-stream\")\n```\n\n## Lifecycle Hooks\n\nDirty Arbiters provide hooks for customization:\n\n```python\n# gunicorn.conf.py\n\ndef on_dirty_starting(arbiter):\n    \"\"\"Called just before the dirty arbiter starts.\"\"\"\n    print(\"Dirty arbiter starting...\")\n\ndef dirty_post_fork(arbiter, worker):\n    \"\"\"Called just after a dirty worker is forked.\"\"\"\n    print(f\"Dirty worker {worker.pid} forked\")\n\ndef dirty_worker_init(worker):\n    \"\"\"Called after a dirty worker initializes all apps.\"\"\"\n    print(f\"Dirty worker {worker.pid} initialized\")\n\ndef dirty_worker_exit(arbiter, worker):\n    \"\"\"Called when a dirty worker exits.\"\"\"\n    print(f\"Dirty worker {worker.pid} exiting\")\n\non_dirty_starting = on_dirty_starting\ndirty_post_fork = dirty_post_fork\ndirty_worker_init = dirty_worker_init\ndirty_worker_exit = dirty_worker_exit\n```\n\n## Signal Handling\n\nDirty Arbiters integrate with the main arbiter's signal handling. Signals are forwarded from the main arbiter to the dirty arbiter, which then propagates them to workers.\n\n### Signal Flow\n\n```\n  Main Arbiter                    Dirty Arbiter                 Dirty Workers\n       |                                |                             |\n  SIGTERM/SIGHUP/SIGUSR1 ------>  signal_handler()                    |\n       |                                |                             |\n       |                          call_soon_threadsafe()              |\n       |                                |                             |\n       |                          handle_signal()                     |\n       |                                |                             |\n       |                                +------> os.kill(worker, sig) |\n       |                                                              |\n```\n\n### Signal Reference\n\n| Signal | At Dirty Arbiter | At Dirty Workers | Notes |\n|--------|-----------------|------------------|-------|\n| `SIGTERM` | Sets `self.alive = False`, waits for graceful shutdown | Exits after completing current request | Graceful shutdown with timeout |\n| `SIGQUIT` | Immediate exit via `sys.exit(0)` | Killed immediately | Fast shutdown, no cleanup |\n| `SIGHUP` | Kills all workers, spawns new ones | Exits immediately | Hot reload of workers |\n| `SIGUSR1` | Reopens log files, forwards to workers | Reopens log files | Log rotation support |\n| `SIGTTIN` | Increases worker count by 1 | N/A | Dynamic scaling up |\n| `SIGTTOU` | Decreases worker count by 1 | N/A | Dynamic scaling down |\n| `SIGCHLD` | Handled by event loop, triggers reap | N/A | Worker death detection |\n| `SIGINT` | Same as SIGTERM | Same as SIGTERM | Ctrl-C handling |\n\n### Dynamic Scaling with TTIN/TTOU\n\nYou can dynamically scale the number of dirty workers at runtime using signals, without restarting gunicorn:\n\n```bash\n# Find the dirty arbiter process\nps aux | grep dirty-arbiter\n# Or use the PID file (location depends on your app name)\ncat /tmp/gunicorn-dirty-myapp.pid\n\n# Increase dirty workers by 1\nkill -TTIN <dirty-arbiter-pid>\n\n# Decrease dirty workers by 1\nkill -TTOU <dirty-arbiter-pid>\n```\n\n**Minimum Worker Constraint:** The dirty arbiter will not decrease below the minimum number of workers required by your app configurations. For example, if you have an app with `workers = 3`, you cannot scale below 3 dirty workers. When this limit is reached, a warning is logged:\n\n```\nWARNING: SIGTTOU: Cannot decrease below 3 workers (required by app specs)\n```\n\n**Use Cases:**\n\n- **Burst handling** - Scale up when you anticipate heavy load\n- **Cost optimization** - Scale down during low-traffic periods\n- **Recovery** - Scale up if workers are busy with long-running tasks\n\n### Forwarded Signals\n\nThe main arbiter forwards these signals to the dirty arbiter process:\n\n- **SIGTERM** - Graceful shutdown of entire process tree\n- **SIGHUP** - Worker reload (main arbiter reloads HTTP workers, dirty arbiter reloads dirty workers)\n- **SIGUSR1** - Log rotation across all processes\n\n### Async Signal Handling\n\nThe dirty arbiter uses asyncio's signal integration for safe handling in the event loop:\n\n```python\n# Signals are registered with the event loop\nloop.add_signal_handler(signal.SIGTERM, self.signal_handler, signal.SIGTERM)\n\ndef signal_handler(self, sig):\n    # Use call_soon_threadsafe for thread-safe event loop integration\n    self.loop.call_soon_threadsafe(self.handle_signal, sig)\n```\n\nThis pattern ensures signals don't interrupt asyncio operations mid-execution, preventing race conditions and partial state updates.\n\n## Liveness and Health Monitoring\n\nDirty Arbiters implement multiple layers of health monitoring to ensure workers remain responsive and orphaned processes are cleaned up.\n\n### Heartbeat Mechanism\n\nEach dirty worker maintains a \"worker tmp\" file whose mtime serves as a heartbeat:\n\n```\nWorker Lifecycle:\n  1. Worker spawns, creates WorkerTmp file\n  2. Worker touches file every (dirty_timeout / 2) seconds\n  3. Arbiter checks all worker mtimes every 1 second\n  4. If mtime > dirty_timeout seconds old, worker is killed\n```\n\nThis file-based heartbeat has several advantages:\n\n- **OS-level tracking** - No IPC required, works even if worker is stuck in C code\n- **Crash detection** - Arbiter notices immediately when worker stops updating\n- **Graceful recovery** - Worker killed with SIGKILL, arbiter spawns replacement\n\n### Timeout Detection\n\nThe arbiter's monitoring loop checks worker health every second:\n\n```python\n# Pseudocode for worker monitoring\nfor worker in self.workers:\n    mtime = worker.tmp.last_update()\n    if time.time() - mtime > self.dirty_timeout:\n        log.warning(f\"Worker {worker.pid} timed out, killing\")\n        os.kill(worker.pid, signal.SIGKILL)\n```\n\nWhen a worker is killed:\n\n1. `SIGCHLD` is delivered to the arbiter\n2. Arbiter reaps the worker process\n3. `dirty_worker_exit` hook is called\n4. A new worker is spawned to maintain `dirty_workers` count\n\n### Parent Death Detection\n\nDirty arbiters monitor their parent process (the main arbiter) to detect orphaning:\n\n```python\n# In the dirty arbiter's main loop\nif os.getppid() != self.parent_pid:\n    log.info(\"Parent died, shutting down\")\n    self.alive = False\n```\n\nThis check runs every iteration of the event loop (typically sub-millisecond). When parent death is detected:\n\n1. Arbiter sets `self.alive = False`\n2. All workers are sent SIGTERM\n3. Arbiter waits for graceful shutdown (up to `dirty_graceful_timeout`)\n4. Remaining workers are sent SIGKILL\n5. Arbiter exits\n\n### Orphan Cleanup\n\nTo handle edge cases where the dirty arbiter itself crashes, a well-known PID file is used:\n\n**PID file location**: `/tmp/gunicorn_dirty_<main_arbiter_pid>.pid`\n\nOn startup, the dirty arbiter:\n\n1. Checks if PID file exists\n2. If yes, reads the old PID and attempts to kill it (`SIGTERM`)\n3. Waits briefly for cleanup\n4. Writes its own PID to the file\n5. On exit, removes the PID file\n\nThis ensures that if a dirty arbiter crashes and the main arbiter restarts it, the old orphaned process is terminated.\n\n### Respawn Behavior\n\n| Component | Respawn Trigger | Respawn Behavior |\n|-----------|-----------------|------------------|\n| Dirty Worker | Exit, timeout, or crash | Immediate respawn to maintain `dirty_workers` count |\n| Dirty Arbiter | Exit or crash | Main arbiter respawns if not shutting down |\n\nThe dirty arbiter maintains a target worker count and continuously spawns workers until the target is reached:\n\n```python\nwhile len(self.workers) < self.num_workers:\n    self.spawn_worker()\n```\n\n### Monitoring Recommendations\n\nFor production deployments, consider:\n\n1. **Log monitoring** - Watch for \"Worker timed out\" messages indicating hung workers\n2. **Process monitoring** - Use systemd or supervisord to monitor the main arbiter\n3. **Metrics** - Track respawn frequency to detect unstable workers\n\n```bash\n# Check for recent worker timeouts\ngrep \"Worker.*timed out\" /var/log/gunicorn.log | tail -20\n\n# Monitor process tree\nwatch -n 1 'pstree -p $(cat gunicorn.pid)'\n```\n\n## Error Handling\n\nThe dirty client raises specific exceptions:\n\n```python\nfrom gunicorn.dirty.errors import (\n    DirtyError,\n    DirtyTimeoutError,\n    DirtyConnectionError,\n    DirtyAppError,\n    DirtyAppNotFoundError,\n    DirtyNoWorkersAvailableError,\n)\n\ntry:\n    result = client.execute(\"myapp.ml:MLApp\", \"inference\", \"model\", data)\nexcept DirtyTimeoutError:\n    # Operation timed out\n    pass\nexcept DirtyAppNotFoundError:\n    # App not loaded in dirty workers\n    pass\nexcept DirtyNoWorkersAvailableError as e:\n    # No workers have this app (all crashed or app limited to 0 workers)\n    print(f\"No workers for app: {e.app_path}\")\nexcept DirtyAppError as e:\n    # Error during app execution\n    print(f\"App error: {e.message}, traceback: {e.traceback}\")\nexcept DirtyConnectionError:\n    # Connection to dirty arbiter failed\n    pass\n```\n\n## Best Practices\n\n1. **Pre-load commonly used resources** in `init()` to avoid cold starts\n2. **Set appropriate timeouts** based on your workload\n3. **Handle errors gracefully** - dirty workers may restart\n4. **Use meaningful action names** for easier debugging\n5. **Keep responses serializable** - results are passed via binary IPC (supports bytes directly)\n\n## Monitoring\n\nMonitor dirty workers using standard process monitoring:\n\n```bash\n# Check dirty arbiter and workers\nps aux | grep \"dirty\"\n\n# View logs\ntail -f gunicorn.log | grep dirty\n```\n\n## Example: Image Processing\n\n```python\n# myapp/images.py\nfrom gunicorn.dirty import DirtyApp\nfrom PIL import Image\nimport io\n\nclass ImageApp(DirtyApp):\n    def init(self):\n        # Pre-import heavy libraries\n        import cv2\n        self.cv2 = cv2\n\n    def resize(self, image_data, width, height):\n        \"\"\"Resize an image.\"\"\"\n        img = Image.open(io.BytesIO(image_data))\n        resized = img.resize((width, height))\n        buffer = io.BytesIO()\n        resized.save(buffer, format='PNG')\n        return buffer.getvalue()\n\n    def thumbnail(self, image_data, size=128):\n        \"\"\"Create a thumbnail.\"\"\"\n        img = Image.open(io.BytesIO(image_data))\n        img.thumbnail((size, size))\n        buffer = io.BytesIO()\n        img.save(buffer, format='JPEG')\n        return buffer.getvalue()\n\n    def close(self):\n        pass\n```\n\nUsage:\n\n```python\nfrom gunicorn.dirty import get_dirty_client\n\ndef upload_image(request):\n    client = get_dirty_client()\n\n    # Create thumbnail in dirty worker\n    thumbnail = client.execute(\n        \"myapp.images:ImageApp\",\n        \"thumbnail\",\n        request.files['image'].read(),\n        size=256\n    )\n\n    return save_thumbnail(thumbnail)\n```\n\n## Complete Examples\n\nFor full working examples with Docker deployment, see:\n\n- [Embedding Service Example](https://github.com/benoitc/gunicorn/tree/master/examples/embedding_service) - FastAPI-based text embedding API using sentence-transformers with dirty workers for ML model management.\n- [Streaming Chat Example](https://github.com/benoitc/gunicorn/tree/master/examples/streaming_chat) - Simulated LLM chat with token-by-token SSE streaming, demonstrating dirty worker generators and real-time response delivery.\n"
  },
  {
    "path": "docs/content/faq.md",
    "content": "<span id=\"faq\"></span>\n# FAQ\n\n## WSGI bits\n\n### How do I set `SCRIPT_NAME`?\n\nBy default `SCRIPT_NAME` is an empty string. Set it via an environment variable\nor HTTP header. Because the header contains an underscore it is only accepted\nfrom trusted forwarders listed in [`forwarded_allow_ips`](reference/settings.md#forwarded_allow_ips).\n\n!!! note\n    If your application should appear under a subfolder, `SCRIPT_NAME` typically\n    starts with a single leading slash and no trailing slash.\n\n\n\n## Server stuff\n\n### How do I reload my application in Gunicorn?\n\nSend `HUP` to the master process for a graceful reload:\n\n```bash\nkill -HUP masterpid\n```\n\n### How might I test a proxy configuration?\n\nUse [Hey](https://github.com/rakyll/hey) to confirm that your proxy buffers\nresponses correctly for synchronous workers:\n\n```bash\nhey -n 10000 -c 100 http://127.0.0.1:5000/\n```\n\nThat benchmark issues 10,000 requests with a concurrency of 100.\n\n### How can I name processes?\n\nInstall [setproctitle](https://pypi.python.org/pypi/setproctitle) to give\nGunicorn processes meaningful names in tools such as `ps` and `top`. This helps\nwhen running multiple Gunicorn instances. See the\n[`proc_name`](reference/settings.md#proc_name) setting for details.\n\n### Why is there no HTTP keep-alive?\n\nThe default sync workers target Nginx, which uses HTTP/1.0 for upstream\nconnections. If you need to serve unbuffered internet traffic directly, pick an\nasync worker instead.\n\n## Worker processes\n\n### How do I know which type of worker to use?\n\nRead the [design guide](design.md) for guidance on worker types.\n\n### What types of workers are available?\n\nSee the [`worker_class`](reference/settings.md#worker_class) configuration reference.\n\n### How can I figure out the best number of worker processes?\n\nFollow the recommendations for tuning the [`number of workers`](design.md#how-many-workers).\n\n### How can I change the number of workers dynamically?\n\nSend `TTIN` or `TTOU` to the master process:\n\n```bash\nkill -TTIN $masterpid  # increment workers\nkill -TTOU $masterpid  # decrement workers\n```\n\n### Does Gunicorn suffer from the thundering herd problem?\n\nPotentially, when many sleeping handlers wake simultaneously but only one takes\nthe request. There is ongoing work to mitigate this\n([issue #792](https://github.com/benoitc/gunicorn/issues/792)). Monitor load if\nyou use large numbers of workers or threads.\n\n### Why don't I see logs in the console?\n\nGunicorn 19.0 disabled console logging by default. Use `--log-file=-` to stream\nlogs to stdout. Console logging returned in 19.2.\n\n## Kernel parameters\n\nHigh-concurrency deployments may need kernel tuning. These Linux-oriented tips\napply to any network service.\n\n### How can I increase the maximum number of file descriptors?\n\nRaise the per-process limit (remember sockets count as files). Running `sudo\nulimit` is ineffective—switch to root, adjust the limit, then launch Gunicorn.\nConsider managing limits via systemd service units or init scripts.\n\n### How can I increase the maximum socket backlog?\n\nIncrease the queue of pending connections:\n\n```bash\nsudo sysctl -w net.core.somaxconn=\"2048\"\n```\n\n### How can I disable the use of `sendfile()`?\n\nPass `--no-sendfile` or set the `SENDFILE=0` environment variable.\n\n## Troubleshooting\n\n### Django reports `ImproperlyConfigured`\n\nAsynchronous workers may break `django.core.urlresolvers.reverse`. Use\n`reverse_lazy` instead.\n\n### How do I avoid blocking in `os.fchmod`?\n\nGunicorn's heartbeat touches temporary files. On disk-backed filesystems (for\nexample `/tmp` on some distributions) `os.fchmod` can block if I/O stalls or the\nfilesystem fills up. Mount a `tmpfs` and point `--worker-tmp-dir` to it.\n\nCheck whether `/tmp` is RAM-backed:\n\n```bash\ndf /tmp\n```\n\nIf not, create a new `tmpfs` mount:\n\n```bash\nsudo cp /etc/fstab /etc/fstab.orig\nsudo mkdir /mem\necho 'tmpfs       /mem tmpfs defaults,size=64m,mode=1777,noatime,comment=for-gunicorn 0 0' | sudo tee -a /etc/fstab\nsudo mount /mem\n```\n\nVerify the result:\n\n```bash\ndf /mem\n```\n\nThen start Gunicorn with `--worker-tmp-dir /mem`.\n\n### Why are workers silently killed?\n\nIf a worker vanishes without logs, check for `SIGKILL`. Reverse proxies may show\n`502` responses while Gunicorn logs only new worker startups (for example,\n`[INFO] Booting worker`). A common culprit is the OOM killer in cgroups-limited\nenvironments.\n\nInspect kernel logs:\n\n```bash\ndmesg | grep gunicorn\n```\n\nIf you see messages similar to `Memory cgroup out of memory ... Killed process\n(gunicorn.md)`, raise memory limits or adjust OOM behaviour.\n"
  },
  {
    "path": "docs/content/guides/docker.md",
    "content": "# Docker Deployment\n\nRunning Gunicorn in Docker containers is the most common deployment pattern\nfor modern Python applications. This guide covers best practices for\ncontainerizing Gunicorn applications.\n\n## Official Docker Image\n\nGunicorn provides an official Docker image on GitHub Container Registry:\n\n```bash\ndocker pull ghcr.io/benoitc/gunicorn:latest\n```\n\n### Quick Start\n\nMount your application directory and run:\n\n```bash\ndocker run -p 8000:8000 -v $(pwd):/app ghcr.io/benoitc/gunicorn app:app\n```\n\n### Running in Background\n\nUse `-d` (detached mode) to run the container in the background:\n\n```bash\n# Start in background\ndocker run -d --name myapp -p 8000:8000 -v $(pwd):/app ghcr.io/benoitc/gunicorn app:app\n\n# View logs\ndocker logs myapp\n\n# Follow logs in real-time\ndocker logs -f myapp\n\n# Stop the container\ndocker stop myapp\n\n# Start it again\ndocker start myapp\n\n# Remove the container\ndocker rm myapp\n```\n\n### Environment Variables\n\n| Variable | Description | Default |\n|----------|-------------|---------|\n| `GUNICORN_BIND` | Full bind address | `0.0.0.0:8000` |\n| `GUNICORN_HOST` | Bind host | `0.0.0.0` |\n| `GUNICORN_PORT` | Bind port | `8000` |\n| `GUNICORN_WORKERS` | Number of workers | `(2 * CPU) + 1` |\n| `GUNICORN_ARGS` | Additional arguments | (none) |\n\n### With Configuration\n\n```bash\ndocker run -p 9000:9000 -v $(pwd):/app \\\n  -e GUNICORN_PORT=9000 \\\n  -e GUNICORN_WORKERS=4 \\\n  -e GUNICORN_ARGS=\"--timeout 120 --access-logfile -\" \\\n  ghcr.io/benoitc/gunicorn app:app\n```\n\n### As Base Image (Recommended for Production)\n\n```dockerfile\nFROM ghcr.io/benoitc/gunicorn:24.1.0\n\n# Install app dependencies\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\n\n# Copy application\nCOPY --chown=gunicorn:gunicorn . .\n\nCMD [\"myapp:app\", \"--workers\", \"4\"]\n```\n\n### With Docker Compose\n\n```yaml\nservices:\n  web:\n    image: ghcr.io/benoitc/gunicorn:latest\n    ports:\n      - \"8000:8000\"\n    volumes:\n      - ./app:/app\n    command: [\"myapp:app\", \"--workers\", \"4\"]\n```\n\n### Available Tags\n\n- `ghcr.io/benoitc/gunicorn:latest` - Latest release\n- `ghcr.io/benoitc/gunicorn:24.1.0` - Specific version\n- `ghcr.io/benoitc/gunicorn:24.1` - Minor version\n- `ghcr.io/benoitc/gunicorn:24` - Major version\n\n## Building Your Own Image\n\nFor more control, build a custom image using the patterns below.\n\n## Basic Dockerfile\n\n```dockerfile\nFROM python:3.12-slim\n\nWORKDIR /app\n\n# Install dependencies\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\n\n# Copy application\nCOPY . .\n\n# Run gunicorn\nCMD [\"gunicorn\", \"app:app\", \"--bind\", \"0.0.0.0:8000\"]\n```\n\nBuild and run:\n\n```bash\ndocker build -t myapp .\ndocker run -p 8000:8000 myapp\n```\n\n## Production Configuration\n\n### Environment Variables\n\nUse environment variables for configuration:\n\n```dockerfile\nFROM python:3.12-slim\n\nWORKDIR /app\n\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\n\nCOPY . .\n\n# Configuration via environment\nENV GUNICORN_WORKERS=4\nENV GUNICORN_BIND=0.0.0.0:8000\n\nCMD gunicorn app:app \\\n    --workers ${GUNICORN_WORKERS} \\\n    --bind ${GUNICORN_BIND}\n```\n\nOr use `GUNICORN_CMD_ARGS`:\n\n```dockerfile\nENV GUNICORN_CMD_ARGS=\"--workers=4 --bind=0.0.0.0:8000\"\nCMD [\"gunicorn\", \"app:app\"]\n```\n\n### Worker Count\n\nIn containers, determine workers based on available CPU:\n\n```python\n# gunicorn.conf.py\nimport multiprocessing\n\nworkers = multiprocessing.cpu_count() * 2 + 1\nbind = \"0.0.0.0:8000\"\n```\n\nOr let Kubernetes/Docker limit CPU and calculate accordingly:\n\n```bash\n# At runtime\ngunicorn app:app --workers $(( 2 * $(nproc) + 1 ))\n```\n\n### Non-Root User\n\nRun as a non-root user for security:\n\n```dockerfile\nFROM python:3.12-slim\n\n# Create non-root user\nRUN useradd --create-home appuser\nWORKDIR /home/appuser/app\n\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\n\nCOPY --chown=appuser:appuser . .\n\nUSER appuser\n\nCMD [\"gunicorn\", \"app:app\", \"--bind\", \"0.0.0.0:8000\"]\n```\n\n### Health Checks\n\nAdd a health check endpoint and Docker health check:\n\n```dockerfile\nHEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \\\n    CMD curl -f http://localhost:8000/health || exit 1\n```\n\n## Multi-Stage Build\n\nReduce image size with multi-stage builds:\n\n```dockerfile\n# Build stage\nFROM python:3.12 AS builder\n\nWORKDIR /app\nCOPY requirements.txt .\nRUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt\n\n# Runtime stage\nFROM python:3.12-slim\n\nWORKDIR /app\n\n# Copy wheels and install\nCOPY --from=builder /wheels /wheels\nRUN pip install --no-cache-dir /wheels/* && rm -rf /wheels\n\nCOPY . .\n\nCMD [\"gunicorn\", \"app:app\", \"--bind\", \"0.0.0.0:8000\", \"--workers\", \"4\"]\n```\n\n## Docker Compose\n\nExample `docker-compose.yml`:\n\n```yaml\nservices:\n  web:\n    build: .\n    ports:\n      - \"8000:8000\"\n    environment:\n      - DATABASE_URL=postgres://db:5432/myapp\n    depends_on:\n      - db\n    deploy:\n      resources:\n        limits:\n          cpus: '2'\n          memory: 512M\n\n  db:\n    image: postgres:15\n    environment:\n      - POSTGRES_DB=myapp\n      - POSTGRES_PASSWORD=secret\n    volumes:\n      - postgres_data:/var/lib/postgresql/data\n\n  nginx:\n    image: nginx:alpine\n    ports:\n      - \"80:80\"\n    volumes:\n      - ./nginx.conf:/etc/nginx/nginx.conf:ro\n    depends_on:\n      - web\n\nvolumes:\n  postgres_data:\n```\n\n## Kubernetes Deployment\n\nExample Kubernetes deployment:\n\n```yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: myapp\nspec:\n  replicas: 3\n  selector:\n    matchLabels:\n      app: myapp\n  template:\n    metadata:\n      labels:\n        app: myapp\n    spec:\n      containers:\n      - name: myapp\n        image: myapp:latest\n        ports:\n        - containerPort: 8000\n        env:\n        - name: GUNICORN_WORKERS\n          value: \"4\"\n        resources:\n          limits:\n            cpu: \"1\"\n            memory: \"512Mi\"\n          requests:\n            cpu: \"500m\"\n            memory: \"256Mi\"\n        livenessProbe:\n          httpGet:\n            path: /health\n            port: 8000\n          initialDelaySeconds: 10\n          periodSeconds: 10\n        readinessProbe:\n          httpGet:\n            path: /health\n            port: 8000\n          initialDelaySeconds: 5\n          periodSeconds: 5\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: myapp\nspec:\n  selector:\n    app: myapp\n  ports:\n  - port: 80\n    targetPort: 8000\n```\n\n## Graceful Shutdown\n\nGunicorn handles `SIGTERM` gracefully by default. Configure the timeout:\n\n```dockerfile\nCMD [\"gunicorn\", \"app:app\", \\\n     \"--bind\", \"0.0.0.0:8000\", \\\n     \"--graceful-timeout\", \"30\", \\\n     \"--timeout\", \"120\"]\n```\n\nMatch Docker's stop timeout:\n\n```yaml\n# docker-compose.yml\nservices:\n  web:\n    stop_grace_period: 30s\n```\n\n## Logging\n\nLog to stdout/stderr for Docker log collection:\n\n```python\n# gunicorn.conf.py\naccesslog = \"-\"\nerrorlog = \"-\"\nloglevel = \"info\"\n```\n\nUse JSON logging for log aggregation:\n\n```python\n# gunicorn.conf.py\nimport json\nimport datetime\n\nclass JsonFormatter:\n    def format(self, record):\n        return json.dumps({\n            \"timestamp\": datetime.datetime.utcnow().isoformat(),\n            \"level\": record.levelname,\n            \"message\": record.getMessage(),\n        })\n\nlogconfig_dict = {\n    \"version\": 1,\n    \"formatters\": {\n        \"json\": {\"()\": JsonFormatter}\n    },\n    \"handlers\": {\n        \"console\": {\n            \"class\": \"logging.StreamHandler\",\n            \"formatter\": \"json\",\n            \"stream\": \"ext://sys.stdout\"\n        }\n    },\n    \"root\": {\n        \"handlers\": [\"console\"],\n        \"level\": \"INFO\"\n    }\n}\n```\n\n## Troubleshooting\n\n### Worker Timeout\n\nIf workers are killed with `[CRITICAL] WORKER TIMEOUT`, increase the timeout:\n\n```bash\ngunicorn app:app --timeout 120\n```\n\nOr investigate slow requests in your application.\n\n### Out of Memory\n\nIf containers are OOM-killed:\n\n1. Reduce worker count\n2. Use `--max-requests` to restart workers periodically\n3. Increase container memory limits\n\n```bash\ngunicorn app:app --workers 2 --max-requests 1000 --max-requests-jitter 100\n```\n\n### Connection Reset\n\nIf you see connection resets, ensure:\n\n1. Load balancer health checks match your `/health` endpoint\n2. Graceful timeout is sufficient for in-flight requests\n3. Keepalive settings match between Gunicorn and upstream proxy\n\n## See Also\n\n- [Deploy](../deploy.md) - General deployment patterns\n- [Settings](../reference/settings.md) - All configuration options\n"
  },
  {
    "path": "docs/content/guides/gunicornc.md",
    "content": "---\ntitle: Control Interface (gunicornc)\nmenu:\n    guides:\n        weight: 15\n---\n\n# Control Interface (gunicornc)\n\nGunicorn provides a control interface similar to [birdc](https://bird.network.cz/?get_doc&v=20&f=bird-3.html) for the BIRD routing daemon. This allows you to inspect and manage a running Gunicorn instance via a Unix socket.\n\n## Overview\n\nThe control interface consists of two parts:\n\n1. **Control Socket Server** - Runs in the arbiter process, accepts commands via Unix socket\n2. **gunicornc CLI** - Interactive client that connects to the control socket\n\n## Quick Start\n\n### Start Gunicorn with Control Socket\n\nBy default, Gunicorn creates a control socket at `gunicorn.ctl` in the current directory:\n\n```bash\ngunicorn -w 4 myapp:app\n```\n\nOr specify a custom path:\n\n```bash\ngunicorn --control-socket /tmp/myapp.ctl -w 4 myapp:app\n```\n\n### Connect with gunicornc\n\n```bash\n# Connect to default socket (./gunicorn.ctl)\ngunicornc\n\n# Connect to custom socket\ngunicornc -s /tmp/myapp.ctl\n\n# Run a single command\ngunicornc -c \"show workers\"\n\n# Output as JSON (for scripting)\ngunicornc -c \"show stats\" -j\n```\n\n## Interactive Mode\n\nWhen run without the `-c` flag, gunicornc enters interactive mode with readline support:\n\n```\n$ gunicornc\nConnected to gunicorn.ctl\nType 'help' for available commands, 'quit' to exit.\n\ngunicorn> show workers\nPID        AGE    BOOTED   LAST_BEAT\n----------------------------------------\n12345      1      yes      0.2s ago\n12346      2      yes      0.1s ago\n12347      3      yes      0.3s ago\n\nTotal: 3 workers\n\ngunicorn> worker add 2\n{\n  \"added\": 2,\n  \"previous\": 3,\n  \"total\": 5\n}\n\ngunicorn> quit\n```\n\n## Commands\n\n### Show Commands\n\n| Command | Description |\n|---------|-------------|\n| `show all` | Overview of all processes (arbiter, web workers, dirty workers) |\n| `show workers` | List HTTP workers with status |\n| `show dirty` | List dirty workers and apps |\n| `show config` | Show current effective configuration |\n| `show stats` | Show server statistics |\n| `show listeners` | Show bound sockets |\n| `help` | Show available commands |\n\n### Worker Management\n\n| Command | Description |\n|---------|-------------|\n| `worker add [N]` | Spawn N workers (default 1) |\n| `worker remove [N]` | Remove N workers (default 1) |\n| `worker kill <PID>` | Gracefully terminate specific worker |\n\n### Dirty Worker Management\n\n| Command | Description |\n|---------|-------------|\n| `dirty add [N]` | Spawn N dirty workers (default 1) |\n| `dirty remove [N]` | Remove N dirty workers (default 1) |\n\n!!! note \"Per-App Worker Limits\"\n    When using `dirty add`, workers only load apps that haven't reached their\n    worker limits. If all apps are at their limits, no new workers will be spawned.\n    The response will include a `reason` field explaining this.\n\n### Server Control\n\n| Command | Description |\n|---------|-------------|\n| `reload` | Graceful reload (equivalent to SIGHUP) |\n| `reopen` | Reopen log files (equivalent to SIGUSR1) |\n| `shutdown [graceful\\|quick]` | Shutdown server (SIGTERM or SIGINT) |\n\n## Example Session\n\n```\n$ gunicornc\nConnected to gunicorn.ctl\nType 'help' for available commands, 'quit' to exit.\n\ngunicorn> show all\nARBITER (master)\n  PID: 12345\n\nWEB WORKERS (4)\n  PID        AGE    BOOTED   LAST_BEAT\n  --------------------------------------\n  12346      1      yes      0.05s ago\n  12347      2      yes      0.04s ago\n  12348      3      yes      0.03s ago\n  12349      4      yes      0.02s ago\n\nDIRTY ARBITER\n  PID: 12350\n\nDIRTY WORKERS (2)\n  PID        AGE    APPS\n  --------------------------------------------------\n  12351      1      MLModel\n                    ImageProcessor\n  12352      2      MLModel\n\ngunicorn> show stats\nUptime:           2h 15m 30s\nPID:              12345\nWorkers current:  4\nWorkers target:   4\nWorkers spawned:  6\nWorkers killed:   2\nReloads:          1\n\ngunicorn> worker add\n{\n  \"added\": 1,\n  \"previous\": 4,\n  \"total\": 5\n}\n\ngunicorn> dirty add 1\n{\n  \"success\": true,\n  \"operation\": \"add\",\n  \"requested\": 1,\n  \"spawned\": 1,\n  \"total_workers\": 3,\n  \"target_workers\": 3\n}\n\ngunicorn> quit\n```\n\n## Configuration\n\n### Settings\n\n| Setting | CLI Flag | Default | Description |\n|---------|----------|---------|-------------|\n| `control_socket` | `--control-socket` | `gunicorn.ctl` | Unix socket path |\n| `control_socket_mode` | `--control-socket-mode` | `0o600` | Socket file permissions |\n| `control_socket_disable` | `--no-control-socket` | `False` | Disable control socket |\n\n### Example Configuration\n\n```python\n# gunicorn.conf.py\nbind = \"0.0.0.0:8000\"\nworkers = 4\n\n# Control socket settings\ncontrol_socket = \"/var/run/gunicorn/myapp.ctl\"\ncontrol_socket_mode = 0o660  # Allow group access\n```\n\n## Scripting\n\nUse the `-j` flag for JSON output when scripting:\n\n```bash\n#!/bin/bash\n\n# Get current worker count\nworkers=$(gunicornc -c \"show stats\" -j | jq -r '.workers_current')\necho \"Current workers: $workers\"\n\n# Scale up if needed\nif [ \"$workers\" -lt 8 ]; then\n    gunicornc -c \"worker add $((8 - workers))\"\nfi\n```\n\n## Security\n\nThe control socket uses filesystem permissions for access control:\n\n- **Default mode**: `0o600` (owner only)\n- **No authentication**: Relies on filesystem permissions\n- **Unix socket only**: No TCP/remote access\n\nTo allow group access:\n\n```python\ncontrol_socket_mode = 0o660\n```\n\nTo disable the control socket entirely:\n\n```bash\ngunicorn --no-control-socket myapp:app\n```\n\n## Protocol\n\nThe control interface uses a JSON-based protocol with length-prefixed framing:\n\n```\n+----------------+------------------+\n| Length (4B BE) |  JSON Payload    |\n+----------------+------------------+\n```\n\n### Request Format\n\n```json\n{\n  \"id\": 1,\n  \"command\": \"show workers\"\n}\n```\n\n### Response Format\n\n```json\n{\n  \"id\": 1,\n  \"status\": \"ok\",\n  \"data\": { ... }\n}\n```\n\n### Error Response\n\n```json\n{\n  \"id\": 1,\n  \"status\": \"error\",\n  \"error\": \"Unknown command: foo\"\n}\n```\n\n## Troubleshooting\n\n### Cannot connect to socket\n\n```\nError: Connection refused\n```\n\n- Check that Gunicorn is running\n- Verify the socket path is correct\n- Check socket file permissions\n\n### Permission denied\n\n```\nError: Permission denied\n```\n\n- Check that you have read/write access to the socket file\n- The socket is created with mode `0o600` by default (owner only)\n\n### Socket not found\n\n```\nError: No such file or directory\n```\n\n- Gunicorn creates the socket relative to the working directory by default\n- Use an absolute path with `--control-socket /path/to/socket.ctl`\n- Check if `--no-control-socket` was specified\n"
  },
  {
    "path": "docs/content/guides/http2.md",
    "content": "# HTTP/2 Support\n\n!!! warning \"Beta Feature\"\n    HTTP/2 support is a beta feature introduced in Gunicorn 25.0.0. While it has been tested,\n    the API and behavior may change in future releases. Please report any issues on\n    [GitHub](https://github.com/benoitc/gunicorn/issues).\n\nGunicorn supports HTTP/2 (RFC 7540) for improved performance with modern clients.\nHTTP/2 provides multiplexed streams, header compression, and other optimizations\nover HTTP/1.1.\n\n## Quick Start\n\n```bash\n# Install gunicorn with HTTP/2 support\npip install gunicorn[http2]\n\n# Run with HTTP/2 enabled (requires SSL)\ngunicorn myapp:app \\\n    --worker-class gthread \\\n    --threads 4 \\\n    --certfile server.crt \\\n    --keyfile server.key \\\n    --http-protocols h2,h1\n```\n\n## Requirements\n\nHTTP/2 support requires:\n\n- **SSL/TLS**: HTTP/2 uses ALPN (Application-Layer Protocol Negotiation) which\n  requires an encrypted connection\n- **h2 library**: Install with `pip install gunicorn[http2]` or `pip install h2`\n- **Compatible worker**: gthread, gevent, or ASGI workers\n\n## Configuration\n\n### Enable HTTP/2\n\nEnable HTTP/2 by setting the `--http-protocols` option:\n\n```bash\ngunicorn myapp:app --http-protocols h2,h1\n```\n\nOr in a configuration file:\n\n```python\n# gunicorn.conf.py\nhttp_protocols = [\"h2\", \"h1\"]\n```\n\nThe order matters for ALPN negotiation - protocols are tried in order of preference.\n\n| Protocol | Description |\n|----------|-------------|\n| `h2`     | HTTP/2 over TLS |\n| `h1`     | HTTP/1.1 (fallback) |\n\n!!! note\n    Always include `h1` as a fallback for clients that don't support HTTP/2.\n\n### SSL/TLS Configuration\n\nHTTP/2 requires SSL/TLS. Configure certificates:\n\n```bash\ngunicorn myapp:app \\\n    --certfile /path/to/server.crt \\\n    --keyfile /path/to/server.key \\\n    --http-protocols h2,h1\n```\n\nOr in a configuration file:\n\n```python\n# gunicorn.conf.py\ncertfile = \"/path/to/server.crt\"\nkeyfile = \"/path/to/server.key\"\nhttp_protocols = [\"h2\", \"h1\"]\n```\n\n### HTTP/2 Settings\n\nFine-tune HTTP/2 behavior with these settings:\n\n| Setting | Default | Description |\n|---------|---------|-------------|\n| `http2_max_concurrent_streams` | 100 | Maximum concurrent streams per connection |\n| `http2_initial_window_size` | 65535 | Initial flow control window size (bytes) |\n| `http2_max_frame_size` | 16384 | Maximum frame size (bytes) |\n| `http2_max_header_list_size` | 65536 | Maximum header list size (bytes) |\n\nExample configuration:\n\n```python\n# gunicorn.conf.py\nhttp_protocols = [\"h2\", \"h1\"]\nhttp2_max_concurrent_streams = 200\nhttp2_initial_window_size = 1048576  # 1MB\n```\n\n## Worker Compatibility\n\nNot all workers support HTTP/2:\n\n| Worker | HTTP/2 Support | Notes |\n|--------|----------------|-------|\n| `sync` | No | Single-threaded, cannot multiplex streams |\n| `gthread` | Yes | Recommended for HTTP/2 |\n| `gevent` | Yes | Requires gevent |\n| `eventlet` | Yes | **Deprecated** - will be removed in 26.0 |\n| `asgi` | Yes | For async frameworks |\n| `tornado` | No | Tornado handles its own protocol |\n\nIf you use the sync or tornado worker with HTTP/2 enabled, Gunicorn will log a\nwarning and fall back to HTTP/1.1.\n\n### Recommended: gthread Worker\n\nFor HTTP/2, the gthread worker is recommended:\n\n```bash\ngunicorn myapp:app \\\n    --worker-class gthread \\\n    --threads 4 \\\n    --workers 2 \\\n    --http-protocols h2,h1 \\\n    --certfile server.crt \\\n    --keyfile server.key\n```\n\n## HTTP 103 Early Hints\n\nGunicorn supports HTTP 103 Early Hints (RFC 8297), allowing servers to send\nresource hints before the final response. This enables browsers to preload\nCSS, JavaScript, and other assets in parallel.\n\n### WSGI Applications\n\nUse the `wsgi.early_hints` callback in your WSGI application:\n\n```python\ndef app(environ, start_response):\n    # Send early hints if available\n    if 'wsgi.early_hints' in environ:\n        environ['wsgi.early_hints']([\n            ('Link', '</style.css>; rel=preload; as=style'),\n            ('Link', '</app.js>; rel=preload; as=script'),\n        ])\n\n    # Continue with the actual response\n    start_response('200 OK', [('Content-Type', 'text/html')])\n    return [b'<html>...</html>']\n```\n\n### ASGI Applications\n\nUse the `http.response.informational` message type:\n\n```python\nasync def app(scope, receive, send):\n    # Send early hints\n    await send({\n        \"type\": \"http.response.informational\",\n        \"status\": 103,\n        \"headers\": [\n            (b\"link\", b\"</style.css>; rel=preload; as=style\"),\n            (b\"link\", b\"</app.js>; rel=preload; as=script\"),\n        ],\n    })\n\n    # Send the actual response\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 200,\n        \"headers\": [(b\"content-type\", b\"text/html\")],\n    })\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": b\"<html>...</html>\",\n    })\n```\n\n!!! note\n    Early hints are only sent to HTTP/1.1+ clients. HTTP/1.0 clients silently\n    ignore the callback since they don't support 1xx responses.\n\n## Stream Priority\n\nHTTP/2 allows clients to indicate the relative priority of streams using PRIORITY frames\n(RFC 7540 Section 5.3). Gunicorn tracks stream priorities and exposes them to both\nWSGI and ASGI applications.\n\n### Accessing Priority in WSGI\n\nPriority information is available in the WSGI environ for HTTP/2 requests:\n\n```python\ndef app(environ, start_response):\n    # Access stream priority (HTTP/2 only)\n    weight = environ.get('gunicorn.http2.priority_weight')\n    depends_on = environ.get('gunicorn.http2.priority_depends_on')\n\n    if weight is not None:\n        # This is an HTTP/2 request with priority info\n        # Higher weight = client considers this more important\n        print(f\"Request priority: weight={weight}, depends_on={depends_on}\")\n\n    start_response('200 OK', [('Content-Type', 'text/plain')])\n    return [b'OK']\n```\n\n| Environ Key | Range | Default | Description |\n|-------------|-------|---------|-------------|\n| `gunicorn.http2.priority_weight` | 1-256 | 16 | Higher weight = more resources |\n| `gunicorn.http2.priority_depends_on` | Stream ID | 0 | Parent stream (0 = root) |\n\n### Accessing Priority in ASGI\n\nFor ASGI applications, priority is available in the scope's `extensions` dict:\n\n```python\nasync def app(scope, receive, send):\n    if scope[\"type\"] == \"http\":\n        # Check for HTTP/2 priority extension\n        extensions = scope.get(\"extensions\", {})\n        priority = extensions.get(\"http.response.priority\")\n\n        if priority:\n            weight = priority[\"weight\"]        # 1-256\n            depends_on = priority[\"depends_on\"]  # Parent stream ID\n            print(f\"Request priority: weight={weight}, depends_on={depends_on}\")\n\n        await send({\n            \"type\": \"http.response.start\",\n            \"status\": 200,\n            \"headers\": [(b\"content-type\", b\"text/plain\")],\n        })\n        await send({\n            \"type\": \"http.response.body\",\n            \"body\": b\"OK\",\n        })\n```\n\n| Extension Key | Field | Range | Default | Description |\n|---------------|-------|-------|---------|-------------|\n| `http.response.priority` | `weight` | 1-256 | 16 | Higher weight = more resources |\n| `http.response.priority` | `depends_on` | Stream ID | 0 | Parent stream (0 = root) |\n\n!!! note\n    Stream priority is advisory. Applications can use it for scheduling decisions,\n    but Gunicorn does not enforce priority-based request ordering. Priority\n    information is only present for HTTP/2 requests.\n\n## Response Trailers\n\nHTTP/2 supports trailing headers (trailers) sent after the response body.\nThis is commonly used for gRPC status codes, checksums, and timing information.\n\n### WSGI Applications\n\nFor WSGI applications, use the `gunicorn.http2.send_trailers` callback in the environ:\n\n```python\ndef app(environ, start_response):\n    # Get trailer callback (HTTP/2 only)\n    send_trailers = environ.get('gunicorn.http2.send_trailers')\n\n    # Announce trailers in response headers\n    headers = [\n        ('Content-Type', 'application/grpc'),\n        ('Trailer', 'grpc-status, grpc-message'),\n    ]\n    start_response('200 OK', headers)\n\n    # Yield response body\n    yield b'response data'\n\n    # Send trailers after body (if available)\n    if send_trailers:\n        send_trailers([\n            ('grpc-status', '0'),\n            ('grpc-message', 'OK'),\n        ])\n```\n\n### ASGI Applications\n\nFor ASGI applications, use the `http.response.trailers` extension:\n\n```python\nasync def app(scope, receive, send):\n    # Send response with trailers flag\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 200,\n        \"headers\": [\n            (b\"content-type\", b\"application/grpc\"),\n            (b\"trailer\", b\"grpc-status, grpc-message\"),\n        ],\n    })\n\n    # Send body\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": b\"response data\",\n        \"more_body\": False,\n    })\n\n    # Send trailers (HTTP/2 only)\n    if \"http.response.trailers\" in scope.get(\"extensions\", {}):\n        await send({\n            \"type\": \"http.response.trailers\",\n            \"headers\": [\n                (b\"grpc-status\", b\"0\"),\n                (b\"grpc-message\", b\"OK\"),\n            ],\n        })\n```\n\n### Trailer Rules (RFC 7540)\n\n- Trailers MUST NOT include pseudo-headers (`:status`, `:path`, etc.)\n- Announce trailers using the `Trailer` response header\n- Trailers are only available in HTTP/2 (HTTP/1.1 chunked encoding not supported)\n\n### Common Use Cases\n\n| Use Case | Trailer Headers |\n|----------|-----------------|\n| gRPC | `grpc-status`, `grpc-message` |\n| Checksums | `Content-MD5`, `Digest` |\n| Timing | `Server-Timing` |\n| Signatures | `Signature` |\n\n## Production Deployment\n\n### With Nginx\n\nConfigure nginx to proxy HTTP/2 connections to Gunicorn:\n\n```nginx\nupstream gunicorn {\n    server 127.0.0.1:8443;\n    keepalive 32;\n}\n\nserver {\n    listen 443 ssl;\n    http2 on;\n    server_name example.com;\n\n    ssl_certificate /path/to/server.crt;\n    ssl_certificate_key /path/to/server.key;\n    ssl_protocols TLSv1.2 TLSv1.3;\n\n    # Forward 103 Early Hints (requires nginx 1.29+)\n    location / {\n        proxy_pass https://gunicorn;\n        proxy_http_version 1.1;\n        proxy_ssl_verify off;\n\n        early_hints $http2;\n\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n    }\n}\n```\n\n!!! note\n    For nginx to forward 103 Early Hints from upstream, you need nginx 1.29+\n    and the [`early_hints`](https://nginx.org/en/docs/http/ngx_http_core_module.html#early_hints) directive.\n\n### Direct TLS Termination\n\nFor simpler deployments, Gunicorn can terminate TLS directly:\n\n```python\n# gunicorn.conf.py\nbind = \"0.0.0.0:443\"\nworker_class = \"gthread\"\nthreads = 4\nworkers = 4\n\n# SSL\ncertfile = \"/etc/letsencrypt/live/example.com/fullchain.pem\"\nkeyfile = \"/etc/letsencrypt/live/example.com/privkey.pem\"\n\n# HTTP/2\nhttp_protocols = [\"h2\", \"h1\"]\nhttp2_max_concurrent_streams = 100\n```\n\n### Recommended Settings\n\nFor production HTTP/2 deployments:\n\n```python\n# gunicorn.conf.py\nworker_class = \"gthread\"\nworkers = 4\nthreads = 4\nkeepalive = 120  # HTTP/2 connections are long-lived\n\n# SSL/TLS\ncertfile = \"/path/to/server.crt\"\nkeyfile = \"/path/to/server.key\"\nssl_version = \"TLSv1_2\"  # Minimum TLS 1.2 for HTTP/2\n\n# HTTP/2\nhttp_protocols = [\"h2\", \"h1\"]\nhttp2_max_concurrent_streams = 100\nhttp2_initial_window_size = 65535\n```\n\n## Troubleshooting\n\n### HTTP/2 not negotiated\n\nIf clients fall back to HTTP/1.1:\n\n1. Verify SSL is configured correctly\n2. Check that `h2` is in `--http-protocols`\n3. Ensure the h2 library is installed: `pip install h2`\n4. Verify ALPN support: `openssl s_client -alpn h2 -connect host:port`\n\n### Worker doesn't support HTTP/2\n\nIf you see \"HTTP/2 is not supported by the sync worker\":\n\n```bash\n# Switch to gthread worker\ngunicorn myapp:app --worker-class gthread --threads 4\n```\n\n### Connection errors with large requests\n\nIncrease flow control window sizes:\n\n```python\nhttp2_initial_window_size = 1048576  # 1MB\nhttp2_max_frame_size = 32768  # 32KB\n```\n\n### Too many concurrent streams\n\nIf clients report stream limit errors:\n\n```python\nhttp2_max_concurrent_streams = 200  # Increase from default 100\n```\n\n## Performance Tuning\n\nHTTP/2 performance depends on proper tuning of both Gunicorn and system settings.\nThis section covers different tuning profiles and their trade-offs.\n\n### Tuning Profiles\n\n#### Conservative (Default)\n\nBest for: Low to moderate traffic, memory-constrained environments.\n\n```python\n# gunicorn.conf.py - Conservative profile\nworkers = 2\nworker_class = \"gthread\"\nthreads = 4\n\nhttp2_max_concurrent_streams = 100\nhttp2_initial_window_size = 65535      # 64KB\nhttp2_max_frame_size = 16384           # 16KB\n```\n\n| Pros | Cons |\n|------|------|\n| Low memory footprint | Limited throughput at high concurrency |\n| Safe defaults per RFC | More round-trips for large transfers |\n| Works on constrained systems | May bottleneck at ~10K req/s |\n\n#### Balanced\n\nBest for: Moderate traffic, general production use.\n\n```python\n# gunicorn.conf.py - Balanced profile\nworkers = 4\nworker_class = \"gthread\"\nthreads = 4\nbacklog = 2048\n\nhttp2_max_concurrent_streams = 128\nhttp2_initial_window_size = 262144     # 256KB\nhttp2_max_frame_size = 16384           # 16KB\n```\n\n| Pros | Cons |\n|------|------|\n| Good throughput (15K+ req/s) | More memory per connection |\n| Handles traffic spikes | Requires more CPU |\n| Good balance of resources | |\n\n#### High Concurrency\n\nBest for: High traffic APIs, microservices, load testing.\n\n```python\n# gunicorn.conf.py - High concurrency profile\nworkers = 4\nworker_class = \"gthread\"\nthreads = 8\nbacklog = 2048\nworker_connections = 10000\n\nhttp2_max_concurrent_streams = 256\nhttp2_initial_window_size = 1048576    # 1MB\nhttp2_max_frame_size = 32768           # 32KB\n```\n\n| Pros | Cons |\n|------|------|\n| High throughput (20K+ req/s) | Higher memory usage (~4x conservative) |\n| Handles 1000s of clients | Requires system tuning |\n| Better large transfer performance | May overwhelm downstream services |\n\n### Setting Trade-offs\n\n#### `http2_max_concurrent_streams`\n\nControls how many simultaneous streams a client can open per connection.\n\n| Value | Memory | Throughput | Use Case |\n|-------|--------|------------|----------|\n| 50-100 | Low | Moderate | APIs with small payloads |\n| 128-256 | Medium | High | General web applications |\n| 500+ | High | Very High | Streaming, real-time apps |\n\n!!! warning\n    Very high values (500+) can lead to resource exhaustion under attack.\n    Use with rate limiting.\n\n#### `http2_initial_window_size`\n\nFlow control window size determines how much data can be sent before waiting for acknowledgment.\n\n| Value | Memory | Latency | Use Case |\n|-------|--------|---------|----------|\n| 65535 (64KB) | Low | Higher for large transfers | Default, memory-constrained |\n| 262144 (256KB) | Medium | Balanced | General use |\n| 1048576 (1MB) | High | Lower for large transfers | Large file transfers, streaming |\n\n!!! note\n    Larger windows improve throughput for large responses but increase memory\n    usage per stream. Calculate: `max_streams × window_size × connections`.\n\n#### `http2_max_frame_size`\n\nMaximum size of individual HTTP/2 frames.\n\n| Value | Memory | Efficiency | Use Case |\n|-------|--------|------------|----------|\n| 16384 (16KB) | Low | More frames for large data | Default, RFC minimum |\n| 32768 (32KB) | Medium | Balanced | General use |\n| 65536 (64KB) | Higher | Fewer frames | Large payloads |\n\n### System Tuning (Linux)\n\nFor high concurrency (1000+ clients), tune these kernel parameters:\n\n```bash\n# /etc/sysctl.conf or /etc/sysctl.d/99-gunicorn.conf\n\n# Increase socket backlog for burst connections\nnet.core.somaxconn = 65535\nnet.ipv4.tcp_max_syn_backlog = 65535\n\n# Increase network queue size\nnet.core.netdev_max_backlog = 65535\n\n# Expand ephemeral port range\nnet.ipv4.ip_local_port_range = 1024 65535\n\n# Allow reuse of TIME_WAIT sockets\nnet.ipv4.tcp_tw_reuse = 1\n\n# Increase max open files system-wide\nfs.file-max = 2097152\n```\n\nApply with: `sudo sysctl -p`\n\nAlso increase file descriptor limits:\n\n```bash\n# /etc/security/limits.conf\n* soft nofile 65535\n* hard nofile 65535\n```\n\n### Docker Tuning\n\nFor Docker deployments, add these to your container or compose file:\n\n```yaml\n# docker-compose.yml\nservices:\n  gunicorn:\n    ulimits:\n      nofile:\n        soft: 65535\n        hard: 65535\n    sysctls:\n      net.core.somaxconn: 65535\n```\n\nOr in Dockerfile:\n\n```dockerfile\n# Increase file descriptor limit\nRUN ulimit -n 65535\n```\n\n### Benchmark Results\n\nReference benchmarks using h2load with 4 Gunicorn workers in Docker (Apple M4 Pro):\n\n| Profile | Clients | Streams | Requests/sec | Latency (mean) |\n|---------|---------|---------|--------------|----------------|\n| Conservative | 100 | 10 | 11,700 | 69ms |\n| Conservative | 1000 | 10 | 12,750 | 441ms |\n| High Concurrency | 100 | 10 | 15,000+ | 50ms |\n| High Concurrency | 1000 | 10 | 21,700 | 253ms |\n| High Concurrency | 2000 | 10 | 12,300 | 243ms |\n\n!!! note\n    Actual performance varies based on hardware, network, and application complexity.\n    Always benchmark your specific workload.\n\n## Testing HTTP/2\n\n### Using curl\n\n```bash\n# Check HTTP/2 support\ncurl -v --http2 https://localhost:443/\n\n# Force HTTP/2\ncurl --http2-prior-knowledge https://localhost:443/\n```\n\n### Using Python\n\n```python\nimport httpx\n\nwith httpx.Client(http2=True, verify=False) as client:\n    response = client.get(\"https://localhost:8443/\")\n    print(f\"HTTP Version: {response.http_version}\")\n```\n\n## Complete Example\n\nA complete HTTP/2 example demonstrating priority and trailers is available in the\n`examples/http2_features/` directory. This includes:\n\n- **http2_app.py**: ASGI application showing priority access and trailer sending\n- **test_http2.py**: Test script verifying HTTP/2 features\n- **Dockerfile** and **docker-compose.yml**: Docker setup for testing\n\nTo run the example:\n\n```bash\ncd examples/http2_features\ndocker compose up --build\n\n# In another terminal:\ndocker compose exec http2-features python /app/http2_features/test_http2.py\n```\n\nThe example demonstrates:\n\n1. **Priority access**: Reading `http.response.priority` extension in ASGI scope\n2. **Response trailers**: Sending `http.response.trailers` messages\n3. **Combined features**: Using both priority and trailers in one response\n\n## RFC Compliance\n\nGunicorn's HTTP/2 implementation is built on the [h2 library](https://github.com/python-hyper/h2)\nand complies with the following specifications:\n\n| Feature | RFC | Status | Notes |\n|---------|-----|--------|-------|\n| HTTP/2 Protocol | [RFC 7540](https://tools.ietf.org/html/rfc7540) | Compliant | Core protocol support |\n| HTTP/2 Semantics | [RFC 9113](https://tools.ietf.org/html/rfc9113) | Compliant | Updated HTTP/2 spec |\n| HPACK Compression | [RFC 7541](https://tools.ietf.org/html/rfc7541) | Compliant | Via h2 library |\n| Stream State Machine | RFC 7540 Section 5.1 | Compliant | Full state transitions |\n| Flow Control | RFC 7540 Section 6.9 | Compliant | Stream and connection level |\n| Stream Priority | RFC 7540 Section 5.3 | Compliant | Weight and dependency tracking |\n| Frame Size Limits | RFC 7540 Section 6.2 | Compliant | Validated 16384-16777215 bytes |\n| Pseudo-Headers | RFC 9113 Section 8.3 | Compliant | All required headers supported |\n| `:authority` Handling | RFC 9113 Section 8.3.1 | Compliant | Takes precedence over Host |\n| Response Trailers | RFC 9110 Section 6.5 | Compliant | Pseudo-headers forbidden |\n| GOAWAY Handling | RFC 7540 Section 6.8 | Compliant | Graceful shutdown |\n| RST_STREAM Handling | RFC 7540 Section 6.4 | Compliant | Stream reset |\n| Early Hints | [RFC 8297](https://tools.ietf.org/html/rfc8297) | Compliant | 103 informational responses |\n| Server Push | RFC 7540 Section 6.6 | Not Implemented | Optional feature, rarely used |\n\n!!! note\n    Server Push (PUSH_PROMISE) is not implemented. This is an optional HTTP/2 feature that is\n    being deprecated in HTTP/3 and is rarely used in practice.\n\n## Security Considerations\n\nHTTP/2 introduces new attack vectors compared to HTTP/1.1. Gunicorn includes several\nprotections against known vulnerabilities.\n\n### Built-in Protections\n\n| Attack | Protection | Setting |\n|--------|------------|---------|\n| Stream Multiplexing Abuse | Limit concurrent streams | `http2_max_concurrent_streams` (default: 100) |\n| HPACK Bomb | Header size limits | `http2_max_header_list_size` (default: 65536) |\n| Large Frame Attack | Frame size limits | `http2_max_frame_size` (validated: 16384-16777215) |\n| Resource Exhaustion | Flow control windows | `http2_initial_window_size` (default: 65535) |\n| Slow Read (Slowloris) | Connection timeouts | `timeout` and `keepalive` settings |\n\n### Recommended Security Settings\n\n```python\n# gunicorn.conf.py - Security-hardened HTTP/2 configuration\n\n# Limit concurrent streams to prevent resource exhaustion\nhttp2_max_concurrent_streams = 100\n\n# Limit header size to prevent HPACK bomb attacks\nhttp2_max_header_list_size = 65536  # 64KB\n\n# Standard frame size (RFC minimum)\nhttp2_max_frame_size = 16384\n\n# Reasonable flow control window\nhttp2_initial_window_size = 65535  # 64KB\n\n# Connection timeouts to prevent slow attacks\ntimeout = 30\nkeepalive = 120\ngraceful_timeout = 30\n\n# Limit request sizes\nlimit_request_line = 4094\nlimit_request_fields = 100\nlimit_request_field_size = 8190\n```\n\n### Additional Recommendations\n\n1. **Use a reverse proxy**: Deploy behind nginx, HAProxy, or a cloud load balancer\n   for additional DDoS protection and rate limiting.\n\n2. **Enable rate limiting**: Use your reverse proxy to limit requests per client.\n\n3. **Monitor connections**: Watch for clients opening many streams or holding\n   connections open without sending data.\n\n4. **Keep dependencies updated**: Regularly update the `h2` library for security fixes.\n\nFor more information on HTTP/2 security vulnerabilities, see:\n\n- [Imperva HTTP/2 Vulnerability Report](https://www.imperva.com/docs/Imperva_HII_HTTP2.pdf)\n- [NGINX HTTP/2 Security Advisory](https://www.nginx.com/blog/the-imperva-http2-vulnerability-report-and-nginx/)\n\n## Compliance Testing\n\n### h2spec\n\n[h2spec](https://github.com/summerwind/h2spec) is the standard conformance testing tool\nfor HTTP/2 implementations. It tests compliance with RFC 7540 and RFC 7541.\n\n```bash\n# Install h2spec\n# macOS\nbrew install h2spec\n\n# Linux (download from releases)\ncurl -L https://github.com/summerwind/h2spec/releases/download/v2.6.0/h2spec_linux_amd64.tar.gz | tar xz\n\n# Run against your server\nh2spec -h localhost -p 8443 -t -k\n\n# Options:\n#   -t    Use TLS\n#   -k    Skip certificate verification\n#   -S    Strict mode (test SHOULD requirements)\n#   -v    Verbose output\n#   -j    Generate JUnit report\n```\n\nExample output:\n```\nGeneric tests for HTTP/2 server\n  1. Starting HTTP/2\n    ✓ Sends a client connection preface\n    ...\n\nHypertext Transfer Protocol Version 2 (HTTP/2)\n  3. Starting HTTP/2\n    3.5. HTTP/2 Connection Preface\n      ✓ Sends invalid connection preface\n      ...\n\n94 tests, 94 passed, 0 skipped, 0 failed\n```\n\n### nghttp2 Tools\n\n[nghttp2](https://nghttp2.org/) provides useful debugging tools:\n\n```bash\n# Install nghttp2\n# macOS\nbrew install nghttp2\n\n# Linux\napt-get install nghttp2-client\n\n# Test HTTP/2 connection\nnghttp -v https://localhost:8443/\n\n# Benchmark with h2load\nh2load -n 1000 -c 10 https://localhost:8443/\n```\n\n### Online Testing\n\nFor public servers, you can use online tools:\n\n- [KeyCDN HTTP/2 Test](https://tools.keycdn.com/http2-test)\n- [HTTP/2 Check](https://http.dev/2/test)\n\n## See Also\n\n- [Settings Reference](../reference/settings.md#http2_max_concurrent_streams) - All HTTP/2 settings\n- [ASGI Worker](../asgi.md) - ASGI worker with HTTP/2 support\n- [Deploy](../deploy.md) - General deployment guidance\n"
  },
  {
    "path": "docs/content/index.md",
    "content": "---\ntemplate: home.html\ntitle: Gunicorn - Python WSGI HTTP Server\n---\n\n<section class=\"hero\">\n  <div class=\"container\">\n    <img class=\"hero__logo\" src=\"assets/gunicorn.svg\" alt=\"Gunicorn\" style=\"width: 350px;\" />\n    <h1>Serve Python on the Web</h1>\n    <p class=\"hero__tagline\">\n      Battle-tested. Production-ready. One command to serve your Python apps.\n    </p>\n    <div class=\"hero__buttons\">\n      <a class=\"btn btn--primary\" href=\"quickstart/\">Get Started</a>\n      <a class=\"btn btn--secondary\" href=\"https://github.com/benoitc/gunicorn\">View on GitHub</a>\n    </div>\n    <div class=\"terminal\">\n      <div class=\"terminal__header\">\n        <span class=\"terminal__dot terminal__dot--red\"></span>\n        <span class=\"terminal__dot terminal__dot--yellow\"></span>\n        <span class=\"terminal__dot terminal__dot--green\"></span>\n      </div>\n      <div class=\"terminal__body\">\n        <span class=\"terminal__line\"><span class=\"terminal__prompt\">$ </span>pip install gunicorn</span>\n        <span class=\"terminal__line\"><span class=\"terminal__prompt\">$ </span>gunicorn myapp:app</span>\n        <span class=\"terminal__line terminal__comment\"># Listening at http://127.0.0.1:8000</span>\n      </div>\n    </div>\n  </div>\n</section>\n\n<section class=\"why\">\n  <div class=\"container\">\n    <h2>Why Gunicorn?</h2>\n    <div class=\"pillars\">\n      <div class=\"pillar\">\n        <h3>Production-Proven</h3>\n        <p>Trusted by thousands of companies. The pre-fork worker model handles traffic spikes gracefully.</p>\n      </div>\n      <div class=\"pillar\">\n        <h3>Lightweight</h3>\n        <p>Minimal dependencies, simple configuration. Efficient from containers to bare metal.</p>\n      </div>\n      <div class=\"pillar\">\n        <h3>Compatible</h3>\n        <p>Works with any WSGI or ASGI framework. Django, Flask, FastAPI—it just runs.</p>\n      </div>\n    </div>\n  </div>\n</section>\n\n<section class=\"frameworks\">\n  <div class=\"container\">\n    <h2>Works With Your Stack</h2>\n    <p class=\"frameworks__subtitle\">WSGI and ASGI frameworks, no changes needed</p>\n    <div class=\"frameworks__list\">\n      <span class=\"framework-tag\">Django</span>\n      <span class=\"framework-tag\">Flask</span>\n      <span class=\"framework-tag framework-tag--new\">FastAPI</span>\n      <span class=\"framework-tag\">Pyramid</span>\n      <span class=\"framework-tag framework-tag--new\">Starlette</span>\n      <span class=\"framework-tag\">Falcon</span>\n      <span class=\"framework-tag\">Bottle</span>\n      <span class=\"framework-tag framework-tag--new\">Quart</span>\n    </div>\n  </div>\n</section>\n\n<section class=\"workers\">\n  <div class=\"container\">\n    <h2>Choose Your Worker</h2>\n    <div class=\"workers__grid\">\n      <a class=\"worker\" href=\"design/#sync-workers\">\n        <h3>Sync</h3>\n        <p>The default. One request per worker. Simple and predictable.</p>\n      </a>\n      <a class=\"worker\" href=\"design/#async-workers\">\n        <h3>Async (Gevent/Eventlet)</h3>\n        <p>Thousands of concurrent connections for I/O-bound workloads.</p>\n      </a>\n      <a class=\"worker\" href=\"reference/settings/#threads\">\n        <h3>Threads</h3>\n        <p>Multiple threads per worker. Balance concurrency and simplicity.</p>\n      </a>\n      <a class=\"worker\" href=\"asgi/\">\n        <h3>ASGI</h3>\n        <p>Native asyncio for FastAPI, Starlette, and async frameworks.</p>\n      </a>\n    </div>\n  </div>\n</section>\n\n<section class=\"quick-links\">\n  <div class=\"container\">\n    <h2>Documentation</h2>\n    <div class=\"quick-links__grid\">\n      <a class=\"quick-link\" href=\"quickstart/\">\n        <strong>Quickstart</strong>\n        <span>Get running in 5 minutes</span>\n      </a>\n      <a class=\"quick-link\" href=\"deploy/\">\n        <strong>Deployment</strong>\n        <span>Nginx, systemd, Docker</span>\n      </a>\n      <a class=\"quick-link\" href=\"reference/settings/\">\n        <strong>Settings</strong>\n        <span>All configuration options</span>\n      </a>\n      <a class=\"quick-link\" href=\"faq/\">\n        <strong>FAQ</strong>\n        <span>Common questions</span>\n      </a>\n    </div>\n  </div>\n</section>\n\n<section class=\"sponsors\">\n  <div class=\"container\">\n    <h2>Support</h2>\n    <p>Powering Python apps since 2010. Support continued development.</p>\n    <a class=\"btn btn--secondary\" href=\"sponsor/\">Become a Sponsor</a>\n  </div>\n</section>\n\n<section class=\"home-footer\">\n  <div class=\"container\">\n    <h2>Join the Community</h2>\n    <p>Questions? Bugs? Ideas? We're here to help.</p>\n    <div class=\"home-footer__links\">\n      <a href=\"https://github.com/benoitc/gunicorn/issues\">GitHub Issues</a>\n      <a href=\"https://web.libera.chat/#gunicorn\">#gunicorn on Libera</a>\n      <a href=\"community/\">Contributing</a>\n    </div>\n  </div>\n</section>\n"
  },
  {
    "path": "docs/content/install.md",
    "content": "# Installation\n\n!!! note\n    Gunicorn requires **Python 3.12 or newer**.\n\n## Quick Install\n\n=== \"pip\"\n\n    ```bash\n    pip install gunicorn\n    ```\n\n=== \"pipx\"\n\n    ```bash\n    pipx install gunicorn\n    ```\n\n=== \"Docker\"\n\n    ```bash\n    docker pull ghcr.io/benoitc/gunicorn:latest\n    docker run -p 8000:8000 -v $(pwd):/app ghcr.io/benoitc/gunicorn app:app\n    ```\n\n    See the [Docker guide](guides/docker.md) for production configurations.\n\n=== \"System Packages\"\n\n    **Debian/Ubuntu:**\n    ```bash\n    sudo apt-get update\n    sudo apt-get install gunicorn\n    ```\n\n    **Fedora:**\n    ```bash\n    sudo dnf install python3-gunicorn\n    ```\n\n    **Arch Linux:**\n    ```bash\n    sudo pacman -S gunicorn\n    ```\n\n    !!! warning\n        System packages may lag behind the latest release. For production,\n        prefer pip installation in a virtual environment.\n\n## Virtual Environment (Recommended)\n\nAlways install Gunicorn inside a virtual environment to isolate dependencies:\n\n```bash\n# Create virtual environment\npython -m venv venv\n\n# Activate it\nsource venv/bin/activate  # Linux/macOS\n# or: venv\\Scripts\\activate  # Windows\n\n# Install gunicorn\npip install gunicorn\n```\n\n## From Source\n\nInstall the latest development version from GitHub:\n\n```bash\npip install git+https://github.com/benoitc/gunicorn.git\n```\n\nUpgrade to the latest commit:\n\n```bash\npip install -U git+https://github.com/benoitc/gunicorn.git\n```\n\n## Extra Packages\n\nGunicorn provides optional extras for additional worker types and features.\nInstall them with pip's bracket syntax:\n\n```bash\npip install gunicorn[gevent,setproctitle]\n```\n\n### Worker Types\n\n| Extra | Description |\n|-------|-------------|\n| `gunicorn[gevent]` | Gevent-based greenlet workers |\n| `gunicorn[gthread]` | Threaded workers |\n| `gunicorn[tornado]` | Tornado-based workers (not recommended) |\n| `gunicorn[eventlet]` | **Deprecated** - will be removed in 26.0 |\n\nSee the [design docs](design.md) for guidance on choosing worker types.\n\n### Utilities\n\n| Extra | Description |\n|-------|-------------|\n| `gunicorn[setproctitle]` | Set process name in `ps`/`top` output |\n\n!!! tip\n    If running multiple Gunicorn instances, use `setproctitle` with the\n    [`proc_name`](reference/settings.md#proc_name) setting to distinguish them.\n\n## Async Workers\n\nFor applications using async I/O patterns, install the appropriate greenlet\nlibrary:\n\n=== \"Gevent\"\n\n    ```bash\n    pip install gunicorn[gevent]\n    ```\n\n    Run with:\n    ```bash\n    gunicorn app:app --worker-class gevent\n    ```\n\n=== \"ASGI (asyncio)\"\n\n    No extra installation required:\n\n    ```bash\n    gunicorn app:app --worker-class asgi\n    ```\n\n    For better performance, install uvloop:\n    ```bash\n    pip install uvloop\n    gunicorn app:app --worker-class asgi --asgi-loop uvloop\n    ```\n\n!!! note\n    Greenlet-based workers require the Python development headers. On Ubuntu:\n    `sudo apt-get install python3-dev`\n\n## Verify Installation\n\nCheck the installed version:\n\n```bash\ngunicorn --version\n```\n\nTest with a simple application:\n\n```bash\necho 'def app(e, s): s(\"200 OK\", []); return [b\"OK\"]' > test_app.py\ngunicorn test_app:app\n# Visit http://127.0.0.1:8000\n```\n\n## Next Steps\n\n- [Quickstart](quickstart.md) - Get running in 5 minutes\n- [Run](run.md) - CLI usage and framework integration\n- [Configure](configure.md) - Configuration options\n"
  },
  {
    "path": "docs/content/instrumentation.md",
    "content": "<span id=\"instrumentation\"></span>\n# Instrumentation\n\n!!! info \"Added in 19.1\"\n    Gunicorn exposes optional instrumentation for the arbiter and workers using the\n    statsD protocol over UDP. The `gunicorn.instrument.statsd` module turns\n    Gunicorn into a statsD client.\n\n\n\nUDP keeps Gunicorn isolated from slow statsD consumers, so metrics collection\ndoes not impact request handling.\n\nTell Gunicorn where the statsD server is located:\n\n```bash\ngunicorn --statsd-host=localhost:8125 --statsd-prefix=service.app ...\n```\n\nThe `Statsd` logger subclasses `gunicorn.glogging.Logger` and tracks:\n\n- `gunicorn.requests` &mdash; request rate per second\n- `gunicorn.request.duration` &mdash; request duration histogram (milliseconds.md)\n- `gunicorn.workers` &mdash; number of workers managed by the arbiter (gauge.md)\n- `gunicorn.log.critical` &mdash; rate of critical log messages\n- `gunicorn.log.error` &mdash; rate of error log messages\n- `gunicorn.log.warning` &mdash; rate of warning log messages\n- `gunicorn.log.exception` &mdash; rate of exceptional log messages\n\nSee the [`statsd_host`](reference/settings.md#statsd_host) setting for additional options.\n\n[statsD](https://github.com/etsy/statsd)\n"
  },
  {
    "path": "docs/content/news.md",
    "content": "<span id=\"news\"></span>\n# Changelog\n\n## unreleased\n\n### Performance\n\n- **ASGI HTTP Parser Optimizations**: Improve ASGI worker HTTP parsing performance\n  - Read chunks in 64-byte blocks instead of 1 byte at a time for chunk size lines and trailers\n  - Reuse BytesIO buffers with truncate/seek instead of creating new objects (reduces GC pressure)\n  - Use `bytearray.find()` directly instead of converting to bytes first\n  - Use index-based iteration for header parsing instead of `list.pop(0)` (O(1) vs O(n))\n\n---\n\n## 25.1.0 - 2026-02-13\n\n### New Features\n\n- **Control Interface (gunicornc)**: Add interactive control interface for managing\n  running Gunicorn instances, similar to birdc for BIRD routing daemon\n  ([PR #3505](https://github.com/benoitc/gunicorn/pull/3505))\n  - Unix socket-based communication with JSON protocol\n  - Interactive mode with readline support and command history\n  - Commands: `show all/workers/dirty/config/stats/listeners`\n  - Worker management: `worker add/remove/kill`, `dirty add/remove`\n  - Server control: `reload`, `reopen`, `shutdown`\n  - New settings: `--control-socket`, `--control-socket-mode`, `--no-control-socket`\n  - New CLI tool: `gunicornc` for connecting to control socket\n  - See [Control Interface Guide](guides/gunicornc.md) for details\n\n- **Dirty Stash**: Add global shared state between workers via `dirty.stash`\n  ([PR #3503](https://github.com/benoitc/gunicorn/pull/3503))\n  - In-memory key-value store accessible by all workers\n  - Supports get, set, delete, clear, keys, and has operations\n  - Useful for sharing state like feature flags, rate limits, or cached data\n\n- **Dirty Binary Protocol**: Implement efficient binary protocol for dirty arbiter IPC\n  using TLV (Type-Length-Value) encoding\n  ([PR #3500](https://github.com/benoitc/gunicorn/pull/3500))\n  - More efficient than JSON for binary data\n  - Supports all Python types: str, bytes, int, float, bool, None, list, dict\n  - Better performance for large payloads\n\n- **Dirty TTIN/TTOU Signals**: Add dynamic worker scaling for dirty arbiters\n  ([PR #3504](https://github.com/benoitc/gunicorn/pull/3504))\n  - Send SIGTTIN to increase dirty workers\n  - Send SIGTTOU to decrease dirty workers\n  - Respects minimum worker constraints from app configurations\n\n### Changes\n\n- **ASGI Worker**: Promoted from beta to stable\n- **Dirty Arbiters**: Now marked as beta feature\n\n### Documentation\n\n- Fix Markdown formatting in /configure documentation\n\n---\n\n## 25.0.3 - 2026-02-07\n\n### Bug Fixes\n\n- Fix RuntimeError when StopIteration is raised inside ASGI response body\n  coroutine (PEP 479 compliance)\n\n- Fix deprecation warning for passing maxsplit as positional argument in\n  `re.split()` (Python 3.13+)\n\n---\n\n## 25.0.2 - 2026-02-06\n\n### Bug Fixes\n\n- Fix ASGI concurrent request failures through nginx proxy by normalizing\n  sockaddr tuples to handle both 2-tuple (IPv4) and 4-tuple (IPv6) formats\n  ([PR #3485](https://github.com/benoitc/gunicorn/pull/3485))\n\n- Fix graceful disconnect handling for ASGI worker to properly handle\n  client disconnects without raising exceptions\n  ([PR #3485](https://github.com/benoitc/gunicorn/pull/3485))\n\n- Fix lazy import of dirty module for gevent compatibility - prevents\n  import errors when concurrent.futures is imported before gevent monkey-patching\n  ([PR #3483](https://github.com/benoitc/gunicorn/pull/3483))\n\n### Changes\n\n- Refactor: Extract `_normalize_sockaddr` utility function for consistent\n  socket address handling across workers\n\n- Add license headers to all Python source files\n\n- Update copyright year to 2026 in LICENSE and NOTICE files\n\n---\n\n## 25.0.1 - 2026-02-02\n\n### Bug Fixes\n\n- Fix ASGI streaming responses (SSE) hanging: add chunked transfer encoding for\n  HTTP/1.1 responses without Content-Length header. Without chunked encoding,\n  clients wait for connection close to determine end-of-response.\n\n### Changes\n\n- Update celery_alternative example to use FastAPI with native ASGI worker and\n  uvloop for async task execution\n\n### Testing\n\n- Add ASGI compliance test suite with Docker-based integration tests covering HTTP,\n  WebSocket, streaming, lifespan, framework integration (Starlette, FastAPI),\n  HTTP/2, and concurrency scenarios\n\n---\n\n## 25.0.0 - 2026-02-01\n\n### New Features\n\n- **Dirty Arbiters**: Separate process pool for executing long-running, blocking\n  operations (AI model loading, heavy computation) without blocking HTTP workers\n  ([PR #3460](https://github.com/benoitc/gunicorn/pull/3460))\n  - Inspired by Erlang's dirty schedulers\n  - Asyncio-based with Unix socket IPC\n  - Stateful workers that persist loaded resources\n  - New settings: `--dirty-app`, `--dirty-workers`, `--dirty-timeout`,\n    `--dirty-threads`, `--dirty-graceful-timeout`\n  - Lifecycle hooks: `on_dirty_starting`, `dirty_post_fork`,\n    `dirty_worker_init`, `dirty_worker_exit`\n\n- **Per-App Worker Allocation for Dirty Arbiters**: Control how many dirty workers\n  load each app for memory optimization with heavy models\n  ([PR #3473](https://github.com/benoitc/gunicorn/pull/3473))\n  - Set `workers` class attribute on DirtyApp (e.g., `workers = 2`)\n  - Or use config format `module:class:N` (e.g., `myapp:HeavyModel:2`)\n  - Requests automatically routed to workers with the target app\n  - New exception `DirtyNoWorkersAvailableError` for graceful error handling\n  - Example: 8 workers × 10GB model = 80GB → with `workers=2`: 20GB (75% savings)\n\n- **HTTP/2 Support (Beta)**: Native HTTP/2 (RFC 7540) support for improved performance\n  with modern clients ([PR #3468](https://github.com/benoitc/gunicorn/pull/3468))\n  - Multiplexed streams over a single connection\n  - Header compression (HPACK)\n  - Flow control and stream prioritization\n  - Works with gthread, gevent, and ASGI workers\n  - New settings: `--http-protocols`, `--http2-max-concurrent-streams`,\n    `--http2-initial-window-size`, `--http2-max-frame-size`, `--http2-max-header-list-size`\n  - Requires SSL/TLS and h2 library: `pip install gunicorn[http2]`\n  - See [HTTP/2 Guide](guides/http2.md) for details\n  - New example: `examples/http2_gevent/` with Docker and tests\n\n- **HTTP 103 Early Hints**: Support for RFC 8297 Early Hints to enable browsers to\n  preload resources before the final response\n  ([PR #3468](https://github.com/benoitc/gunicorn/pull/3468))\n  - WSGI: `environ['wsgi.early_hints'](headers)` callback\n  - ASGI: `http.response.informational` message type\n  - Works with both HTTP/1.1 and HTTP/2\n\n- **uWSGI Protocol for ASGI Worker**: The ASGI worker now supports receiving requests\n  via the uWSGI binary protocol from nginx\n  ([PR #3467](https://github.com/benoitc/gunicorn/pull/3467))\n\n### Bug Fixes\n\n- Fix HTTP/2 ALPN negotiation for gevent and eventlet workers when\n  `do_handshake_on_connect` is False (the default). The TLS handshake is now\n  explicitly performed before checking `selected_alpn_protocol()`.\n\n- Fix setproctitle initialization with systemd socket activation\n  ([#3465](https://github.com/benoitc/gunicorn/issues/3465))\n\n- Fix `Expect: 100-continue` handling: ignore the header for HTTP/1.0 requests\n  since 100-continue is only valid for HTTP/1.1+\n  ([PR #3463](https://github.com/benoitc/gunicorn/pull/3463))\n\n- Fix missing `_expected_100_continue` attribute in UWSGIRequest\n\n- Disable setproctitle on macOS to prevent segfaults during process title updates\n\n- Publish full exception traceback when the application fails to load\n  ([#3462](https://github.com/benoitc/gunicorn/issues/3462))\n\n- Fix ASGI: quick shutdown on SIGINT/SIGQUIT, graceful on SIGTERM\n\n### Deprecations\n\n- **Eventlet Worker**: The `eventlet` worker is deprecated and will be removed in\n  Gunicorn 26.0. Eventlet itself is [no longer actively maintained](https://eventlet.readthedocs.io/en/latest/asyncio/migration.html).\n  Please migrate to `gevent`, `gthread`, or another supported worker type.\n\n### Changes\n\n- Remove obsolete Makefile targets\n  ([PR #3471](https://github.com/benoitc/gunicorn/pull/3471))\n\n---\n\n## 24.1.1 - 2026-01-24\n\n### Bug Fixes\n\n- Fix `forwarded_allow_ips` and `proxy_allow_ips` to remain as strings for backward\n  compatibility with external tools like uvicorn. Network validation now uses strict\n  mode to detect invalid CIDR notation (e.g., `192.168.1.1/24` where host bits are set)\n  ([#3458](https://github.com/benoitc/gunicorn/issues/3458),\n  [PR #3459](https://github.com/benoitc/gunicorn/pull/3459))\n\n---\n\n## 24.1.0 - 2026-01-23\n\n### New Features\n\n- **Official Docker Image**: Gunicorn now publishes official Docker images to GitHub\n  Container Registry at `ghcr.io/benoitc/gunicorn`\n  - Based on Python 3.12 slim image\n  - Uses recommended worker formula (2 × CPU + 1)\n  - Configurable via environment variables\n\n- **PROXY Protocol v2 Support**: Extended PROXY protocol implementation to support\n  the binary v2 format in addition to the existing text-based v1 format\n  - New `--proxy-protocol` modes: `off`, `v1`, `v2`, `auto`\n  - Works with HAProxy, AWS NLB/ALB, and other PROXY protocol v2 sources\n\n- **CIDR Network Support**: `--forwarded-allow-ips` and `--proxy-allow-from` now\n  accept CIDR notation (e.g., `192.168.0.0/16`) for specifying trusted networks\n\n- **Socket Backlog Metric**: New `gunicorn.socket.backlog` gauge metric reports\n  the current socket backlog size on Linux systems\n\n- **InotifyReloader Enhancement**: The inotify-based reloader now watches newly\n  imported modules, not just those loaded at startup\n\n### Bug Fixes\n\n- Fix signal handling regression where SIGCLD alias caused errors on Linux\n- Fix socket blocking mode on keepalive connections with async workers\n- Handle `SSLWantReadError` in `finish_body()` to prevent worker hangs\n- Log SIGTERM as info level instead of warning\n- Print exception details to stderr when worker fails to boot\n- Fix `unreader.unread()` to prepend data to buffer instead of appending\n- Prevent `RecursionError` when pickling Config objects\n\n---\n\n## 24.0.0 - 2026-01-23\n\n### New Features\n\n- **ASGI Worker (Beta)**: Native asyncio-based ASGI support for running async Python\n  frameworks like FastAPI, Starlette, and Quart without external dependencies\n  - HTTP/1.1 with keepalive connections\n  - WebSocket support\n  - Lifespan protocol for startup/shutdown hooks\n  - Optional uvloop for improved performance\n\n- **uWSGI Binary Protocol**: Support for receiving requests from nginx via\n  `uwsgi_pass` directive\n\n- **Documentation Migration**: Migrated to MkDocs with Material theme\n\n### Security\n\n- **eventlet**: Require eventlet >= 0.40.3 (CVE-2021-21419, CVE-2025-58068)\n- **gevent**: Require gevent >= 24.10.1 (CVE-2023-41419, CVE-2024-3219)\n- **tornado**: Require tornado >= 6.5.0 (CVE-2025-47287)\n\n---\n\n## 23.0.0 - 2024-08-10\n\n- minor docs fixes ([PR #3217](https://github.com/benoitc/gunicorn/pull/3217), [PR #3089](https://github.com/benoitc/gunicorn/pull/3089), [PR #3167](https://github.com/benoitc/gunicorn/pull/3167))\n- worker_class parameter accepts a class ([PR #3079](https://github.com/benoitc/gunicorn/pull/3079))\n- fix deadlock if request terminated during chunked parsing ([PR #2688](https://github.com/benoitc/gunicorn/pull/2688))\n- permit receiving Transfer-Encodings: compress, deflate, gzip ([PR #3261](https://github.com/benoitc/gunicorn/pull/3261))\n- permit Transfer-Encoding headers specifying multiple encodings. note: no parameters, still ([PR #3261](https://github.com/benoitc/gunicorn/pull/3261))\n- sdist generation now explicitly excludes sphinx build folder ([PR #3257](https://github.com/benoitc/gunicorn/pull/3257))\n- decode bytes-typed status (as can be passed by gevent) as utf-8 instead of raising `TypeError` ([PR #2336](https://github.com/benoitc/gunicorn/pull/2336))\n- raise correct Exception when encounting invalid chunked requests ([PR #3258](https://github.com/benoitc/gunicorn/pull/3258))\n- the SCRIPT_NAME and PATH_INFO headers, when received from allowed forwarders, are no longer restricted for containing an underscore ([PR #3192](https://github.com/benoitc/gunicorn/pull/3192))\n- include IPv6 loopback address ``[::1]`` in default for [forwarded-allow-ips](reference/settings.md#forwarded_allow_ips) and [proxy-allow-ips](reference/settings.md#proxy_allow_ips) ([PR #3192](https://github.com/benoitc/gunicorn/pull/3192))\n\n!!! note\n    - The SCRIPT_NAME change mitigates a regression that appeared first in the 22.0.0 release\n    - Review your [forwarded-allow-ips](reference/settings.md#forwarded_allow_ips) setting if you are still not seeing the SCRIPT_NAME transmitted\n    - Review your [forwarder-headers](reference/settings.md#forwarder_headers) setting if you are missing headers after upgrading from a version prior to 22.0.0\n\n\n### Breaking changes\n\n- refuse requests where the uri field is empty ([PR #3255](https://github.com/benoitc/gunicorn/pull/3255))\n- refuse requests with invalid CR/LR/NUL in heade field values ([PR #3253](https://github.com/benoitc/gunicorn/pull/3253))\n- remove temporary ``--tolerate-dangerous-framing`` switch from 22.0 ([PR #3260](https://github.com/benoitc/gunicorn/pull/3260))\n- If any of the breaking changes affect you, be aware that now refused requests can post a security problem, especially so in setups involving request pipe-lining and/or proxies.\n\n## 22.0.0 - 2024-04-17\n\n- use `utime` to notify workers liveness\n- migrate setup to pyproject.toml\n- fix numerous security vulnerabilities in HTTP parser (closing some request smuggling vectors)\n- parsing additional requests is no longer attempted past unsupported request framing\n- on HTTP versions < 1.1 support for chunked transfer is refused (only used in exploits)\n- requests conflicting configured or passed SCRIPT_NAME now produce a verbose error\n- Trailer fields are no longer inspected for headers indicating secure scheme\n- support Python 3.12\n\n### Breaking changes\n\n- minimum version is Python 3.7\n- the limitations on valid characters in the HTTP method have been bounded to Internet Standards\n- requests specifying unsupported transfer coding (order.md) are refused by default (rare.md)\n- HTTP methods are no longer casefolded by default (IANA method registry contains none affected)\n- HTTP methods containing the number sign (#) are no longer accepted by default (rare.md)\n- HTTP versions < 1.0 or >= 2.0 are no longer accepted by default (rare, only HTTP/1.1 is supported)\n- HTTP versions consisting of multiple digits or containing a prefix/suffix are no longer accepted\n- HTTP header field names Gunicorn cannot safely map to variables are silently dropped, as in other software\n- HTTP headers with empty field name are refused by default (no legitimate use cases, used in exploits)\n- requests with both Transfer-Encoding and Content-Length are refused by default (such a message might indicate an attempt to perform request smuggling)\n- empty transfer codings are no longer permitted (reportedly seen with really old & broken proxies)\n\n\n### Security\n\n- fix CVE-2024-1135\n\n## History\n\n- [2026](2026-news.md)\n- [2024](2024-news.md)\n- [2023](2023-news.md)\n- [2021](2021-news.md)\n- [2020](2020-news.md)\n- [2019](2019-news.md)\n- [2018](2018-news.md)\n- [2017](2017-news.md)\n- [2016](2016-news.md)\n- [2015](2015-news.md)\n- [2014](2014-news.md)\n- [2013](2013-news.md)\n- [2012](2012-news.md)\n- [2011](2011-news.md)\n- [2010](2010-news.md)\n"
  },
  {
    "path": "docs/content/quickstart.md",
    "content": "# Quickstart\n\nGet a Python web application running with Gunicorn in 5 minutes.\n\n## Install\n\n```bash\npip install gunicorn\n```\n\n## Create an Application\n\nCreate `app.py`:\n\n=== \"Flask\"\n\n    ```python\n    from flask import Flask\n\n    app = Flask(__name__)\n\n    @app.route(\"/\")\n    def hello():\n        return \"Hello, World!\"\n    ```\n\n=== \"FastAPI\"\n\n    ```python\n    from fastapi import FastAPI\n\n    app = FastAPI()\n\n    @app.get(\"/\")\n    def hello():\n        return {\"message\": \"Hello, World!\"}\n    ```\n\n=== \"Django\"\n\n    Django projects already have a WSGI application at `myproject/wsgi.py`.\n    No additional code is needed.\n\n=== \"Plain WSGI\"\n\n    ```python\n    def app(environ, start_response):\n        data = b\"Hello, World!\"\n        start_response(\"200 OK\", [\n            (\"Content-Type\", \"text/plain\"),\n            (\"Content-Length\", str(len(data)))\n        ])\n        return [data]\n    ```\n\n## Run\n\n```bash\ngunicorn app:app\n```\n\nFor Django:\n\n```bash\ngunicorn myproject.wsgi\n```\n\nFor FastAPI (ASGI):\n\n```bash\ngunicorn app:app --worker-class asgi\n```\n\n## Add Workers\n\nUse multiple workers to handle concurrent requests:\n\n```bash\ngunicorn app:app --workers 4\n```\n\nA good starting point is `2 * CPU_CORES + 1` workers.\n\n## Bind to a Port\n\nBy default Gunicorn binds to `127.0.0.1:8000`. Change it with:\n\n```bash\ngunicorn app:app --bind 0.0.0.0:8080\n```\n\n## Configuration File\n\nCreate `gunicorn.conf.py` for reusable settings:\n\n```python\nbind = \"0.0.0.0:8000\"\nworkers = 4\naccesslog = \"-\"\n```\n\nThen run:\n\n```bash\ngunicorn app:app\n```\n\nGunicorn automatically loads `gunicorn.conf.py` from the current directory.\n\n## Next Steps\n\n- [Run](run.md) - Full CLI reference and framework integration\n- [Configure](configure.md) - Configuration file options\n- [Deploy](deploy.md) - Production deployment with nginx and process managers\n- [Settings](reference/settings.md) - Complete settings reference\n"
  },
  {
    "path": "docs/content/reference/settings.md",
    "content": "> **Generated file** — update `gunicorn/config.py` instead.\n\n# Settings\n\nThis reference is built directly from `gunicorn.config.KNOWN_SETTINGS` and is\nregenerated during every documentation build.\n\n!!! note\n    Settings can be provided through the `GUNICORN_CMD_ARGS` environment\n    variable. For example:\n\n    ```console\n    $ GUNICORN_CMD_ARGS=\"--bind=127.0.0.1 --workers=3\" gunicorn app:app\n    ```\n\n    _Added in 19.7._\n\n\n<span id=\"blocking_os_fchmod\"></span>\n\n## Config File\n\n### `config`\n\n**Command line:** `-c CONFIG`, `--config CONFIG`\n\n**Default:** `'./gunicorn.conf.py'`\n\n[The Gunicorn config file](../configure.md#configuration-file).\n\nA string of the form ``PATH``, ``file:PATH``, or ``python:MODULE_NAME``.\n\nOnly has an effect when specified on the command line or as part of an\napplication specific configuration.\n\nBy default, a file named ``gunicorn.conf.py`` will be read from the same\ndirectory where gunicorn is being run.\n\n!!! info \"Changed in 19.4\"\n    Loading the config from a Python module requires the ``python:``\n    prefix.\n\n### `wsgi_app`\n\n**Default:** `None`\n\nA WSGI application path in pattern ``$(MODULE_NAME):$(VARIABLE_NAME)``.\n\n!!! info \"Added in 20.1.0\"\n\n## Control\n\n### `control_socket`\n\n**Command line:** `--control-socket PATH`\n\n**Default:** `'/run/gunicorn.ctl'`\n\nUnix socket path for control interface.\n\nThe control socket allows runtime management of Gunicorn via the\n``gunicornc`` command-line tool. Commands include viewing worker\nstatus, adjusting worker count, and graceful reload/shutdown.\n\nBy default, creates ``/run/gunicorn.ctl`` (requires write access to\n``/run``). For user-level deployments, specify a different path such\nas ``/tmp/gunicorn.ctl`` or ``~/.gunicorn.ctl``.\n\nUse ``--no-control-socket`` to disable.\n\n!!! info \"Added in 25.1.0\"\n\n### `control_socket_mode`\n\n**Command line:** `--control-socket-mode INT`\n\n**Default:** `384`\n\nPermission mode for control socket.\n\nRestricts who can connect to the control socket. Default ``0600``\nallows only the socket owner. Set to ``0660`` to allow group access.\n\n!!! info \"Added in 25.1.0\"\n\n### `control_socket_disable`\n\n**Command line:** `--no-control-socket`\n\n**Default:** `False`\n\nDisable control socket.\n\nWhen set, no control socket is created and ``gunicornc`` cannot\nconnect to this Gunicorn instance.\n\n!!! info \"Added in 25.1.0\"\n\n## Debugging\n\n### `reload`\n\n**Command line:** `--reload`\n\n**Default:** `False`\n\nRestart workers when code changes.\n\nThis setting is intended for development. It will cause workers to be\nrestarted whenever application code changes.\n\nThe reloader is incompatible with application preloading. When using a\npaste configuration be sure that the server block does not import any\napplication code or the reload will not work as designed.\n\nThe default behavior is to attempt inotify with a fallback to file\nsystem polling. Generally, inotify should be preferred if available\nbecause it consumes less system resources.\n\n!!! note\n    In order to use the inotify reloader, you must have the ``inotify``\n    package installed.\n\n!!! warning\n    Enabling this will change what happens on failure to load the\n    the application: While the reloader is active, any and all clients\n    that can make requests can see the full exception and traceback!\n\n### `reload_engine`\n\n**Command line:** `--reload-engine STRING`\n\n**Default:** `'auto'`\n\nThe implementation that should be used to power [reload](#reload).\n\nValid engines are:\n\n* ``'auto'``\n* ``'poll'``\n* ``'inotify'`` (requires inotify)\n\n!!! info \"Added in 19.7\"\n\n### `reload_extra_files`\n\n**Command line:** `--reload-extra-file FILES`\n\n**Default:** `[]`\n\nExtends [reload](#reload) option to also watch and reload on additional files\n(e.g., templates, configurations, specifications, etc.).\n\n!!! info \"Added in 19.8\"\n\n### `spew`\n\n**Command line:** `--spew`\n\n**Default:** `False`\n\nInstall a trace function that spews every line executed by the server.\n\nThis is the nuclear option.\n\n### `check_config`\n\n**Command line:** `--check-config`\n\n**Default:** `False`\n\nCheck the configuration and exit. The exit status is 0 if the\nconfiguration is correct, and 1 if the configuration is incorrect.\n\n### `print_config`\n\n**Command line:** `--print-config`\n\n**Default:** `False`\n\nPrint the configuration settings as fully resolved. Implies [check-config](#check_config).\n\n## Dirty Arbiter Hooks\n\n### `on_dirty_starting`\n\n**Default:**\n\n```python\ndef on_dirty_starting(arbiter):\n    pass\n```\n\nCalled just before the dirty arbiter process is initialized.\n\nThe callable needs to accept a single instance variable for the\nDirtyArbiter.\n\n!!! info \"Added in 25.0.0\"\n\n### `dirty_post_fork`\n\n**Default:**\n\n```python\ndef dirty_post_fork(arbiter, worker):\n    pass\n```\n\nCalled just after a dirty worker has been forked.\n\nThe callable needs to accept two instance variables for the\nDirtyArbiter and new DirtyWorker.\n\n!!! info \"Added in 25.0.0\"\n\n### `dirty_worker_init`\n\n**Default:**\n\n```python\ndef dirty_worker_init(worker):\n    pass\n```\n\nCalled just after a dirty worker has initialized all applications.\n\nThe callable needs to accept one instance variable for the\nDirtyWorker.\n\n!!! info \"Added in 25.0.0\"\n\n### `dirty_worker_exit`\n\n**Default:**\n\n```python\ndef dirty_worker_exit(arbiter, worker):\n    pass\n```\n\nCalled when a dirty worker has exited.\n\nThe callable needs to accept two instance variables for the\nDirtyArbiter and the exiting DirtyWorker.\n\n!!! info \"Added in 25.0.0\"\n\n## Dirty Arbiters\n\n### `dirty_apps`\n\n**Command line:** `--dirty-app STRING`\n\n**Default:** `[]`\n\nDirty applications to load in the dirty worker pool.\n\nA list of application paths in one of these formats:\n\n- ``$(MODULE_NAME):$(CLASS_NAME)`` - all workers load this app\n- ``$(MODULE_NAME):$(CLASS_NAME):$(N)`` - only N workers load this app\n\nEach dirty app must be a class that inherits from ``DirtyApp`` base class\nand implements the ``init()``, ``__call__()``, and ``close()`` methods.\n\nExample::\n\n    dirty_apps = [\n        \"myapp.ml:MLApp\",           # All workers load this\n        \"myapp.images:ImageApp\",    # All workers load this\n        \"myapp.heavy:HugeModel:2\",  # Only 2 workers load this\n    ]\n\nThe per-app worker limit is useful for memory-intensive applications\nlike large ML models. Instead of all 8 workers loading a 10GB model\n(80GB total), you can limit it to 2 workers (20GB total).\n\nAlternatively, you can set the ``workers`` class attribute on your\nDirtyApp subclass::\n\n    class HugeModelApp(DirtyApp):\n        workers = 2  # Only 2 workers load this app\n\n        def init(self):\n            self.model = load_10gb_model()\n\nNote: The config format (``module:Class:N``) takes precedence over\nthe class attribute if both are specified.\n\nDirty apps are loaded once when the dirty worker starts and persist\nin memory for the lifetime of the worker. This is ideal for loading\nML models, database connection pools, or other stateful resources\nthat are expensive to initialize.\n\n!!! info \"Added in 25.0.0\"\n\n!!! info \"Changed in 25.1.0\"\n    Added per-app worker allocation via ``:N`` format suffix.\n\n### `dirty_workers`\n\n**Command line:** `--dirty-workers INT`\n\n**Default:** `0`\n\nThe number of dirty worker processes.\n\nA positive integer. Set to 0 (default) to disable the dirty arbiter.\nWhen set to a positive value, a dirty arbiter process will be spawned\nto manage the dirty worker pool.\n\nDirty workers are separate from HTTP workers and are designed for\nlong-running, blocking operations like ML model inference or heavy\ncomputation.\n\n!!! info \"Added in 25.0.0\"\n\n### `dirty_timeout`\n\n**Command line:** `--dirty-timeout INT`\n\n**Default:** `300`\n\nTimeout for dirty task execution in seconds.\n\nWorkers silent for more than this many seconds are considered stuck\nand will be killed. Set to a high value for operations like model\nloading that may take a long time.\n\nValue is a positive number. Setting it to 0 disables timeout checking.\n\n!!! info \"Added in 25.0.0\"\n\n### `dirty_threads`\n\n**Command line:** `--dirty-threads INT`\n\n**Default:** `1`\n\nThe number of threads per dirty worker.\n\nEach dirty worker can use threads to handle concurrent operations\nwithin the same process, useful for async-safe applications.\n\n!!! info \"Added in 25.0.0\"\n\n### `dirty_graceful_timeout`\n\n**Command line:** `--dirty-graceful-timeout INT`\n\n**Default:** `30`\n\nTimeout for graceful dirty worker shutdown in seconds.\n\nAfter receiving a shutdown signal, dirty workers have this much time\nto finish their current tasks. Workers still alive after the timeout\nare force killed.\n\n!!! info \"Added in 25.0.0\"\n\n## HTTP/2\n\n### `http_protocols`\n\n**Command line:** `--http-protocols STRING`\n\n**Default:** `'h1'`\n\nHTTP protocol versions to support (comma-separated, order = preference).\n\nValid protocols:\n\n* ``h1`` - HTTP/1.1 (default)\n* ``h2`` - HTTP/2 (requires TLS with ALPN)\n* ``h3`` - HTTP/3 (future, not yet implemented)\n\nExamples::\n\n    # HTTP/1.1 only (default, backward compatible)\n    --http-protocols=h1\n\n    # Prefer HTTP/2, fallback to HTTP/1.1\n    --http-protocols=h2,h1\n\n    # HTTP/2 only (reject HTTP/1.1 clients)\n    --http-protocols=h2\n\nHTTP/2 requires:\n\n* TLS (--certfile and --keyfile)\n* The h2 library: ``pip install gunicorn[http2]``\n* ALPN-capable TLS client\n\n!!! note\n    HTTP/2 cleartext (h2c) is not supported due to security concerns\n    and lack of browser support.\n\n!!! info \"Added in 25.0.0\"\n\n### `http2_max_concurrent_streams`\n\n**Command line:** `--http2-max-concurrent-streams INT`\n\n**Default:** `100`\n\nMaximum number of concurrent HTTP/2 streams per connection.\n\nThis limits how many requests can be processed simultaneously on a\nsingle HTTP/2 connection. Higher values allow more parallelism but\nuse more memory.\n\nDefault is 100, which matches common server configurations.\nThe HTTP/2 specification allows up to 2^31-1.\n\n!!! info \"Added in 25.0.0\"\n\n### `http2_initial_window_size`\n\n**Command line:** `--http2-initial-window-size INT`\n\n**Default:** `65535`\n\nInitial HTTP/2 flow control window size in bytes.\n\nThis controls how much data can be in-flight before the receiver\nsends WINDOW_UPDATE frames. Larger values can improve throughput\nfor large transfers but use more memory.\n\nDefault is 65535 (64KB - 1), the HTTP/2 specification default.\nMaximum is 2^31-1 (2147483647).\n\n!!! info \"Added in 25.0.0\"\n\n### `http2_max_frame_size`\n\n**Command line:** `--http2-max-frame-size INT`\n\n**Default:** `16384`\n\nMaximum HTTP/2 frame payload size in bytes.\n\nThis is the largest frame payload the server will accept.\nLarger frames reduce framing overhead but may increase latency\nfor small messages.\n\nDefault is 16384 (16KB), the HTTP/2 specification minimum.\nRange is 16384 to 16777215 (16MB - 1).\n\n!!! info \"Added in 25.0.0\"\n\n### `http2_max_header_list_size`\n\n**Command line:** `--http2-max-header-list-size INT`\n\n**Default:** `65536`\n\nMaximum size of HTTP/2 header list in bytes (HPACK protection).\n\nThis limits the total size of headers after HPACK decompression.\nProtects against compression bombs and excessive memory use.\n\nDefault is 65536 (64KB). Set to 0 for unlimited (not recommended).\n\n!!! info \"Added in 25.0.0\"\n\n## Logging\n\n### `accesslog`\n\n**Command line:** `--access-logfile FILE`\n\n**Default:** `None`\n\nThe Access log file to write to.\n\n``'-'`` means log to stdout.\n\n### `disable_redirect_access_to_syslog`\n\n**Command line:** `--disable-redirect-access-to-syslog`\n\n**Default:** `False`\n\nDisable redirect access logs to syslog.\n\n!!! info \"Added in 19.8\"\n\n### `access_log_format`\n\n**Command line:** `--access-logformat STRING`\n\n**Default:** `'%(h)s %(l)s %(u)s %(t)s \"%(r)s\" %(s)s %(b)s \"%(f)s\" \"%(a)s\"'`\n\nThe access log format.\n\n===========  ===========\nIdentifier   Description\n===========  ===========\nh            remote address\nl            ``'-'``\nu            user name (if HTTP Basic auth used)\nt            date of the request\nr            status line (e.g. ``GET / HTTP/1.1``)\nm            request method\nU            URL path without query string\nq            query string\nH            protocol\ns            status\nB            response length\nb            response length or ``'-'`` (CLF format)\nf            referrer (note: header is ``referer``)\na            user agent\nT            request time in seconds\nM            request time in milliseconds\nD            request time in microseconds\nL            request time in decimal seconds\np            process ID\n{header}i    request header\n{header}o    response header\n{variable}e  environment variable\n===========  ===========\n\nUse lowercase for header and environment variable names, and put\n``{...}x`` names inside ``%(...)s``. For example::\n\n    %({x-forwarded-for}i)s\n\n### `errorlog`\n\n**Command line:** `--error-logfile FILE`, `--log-file FILE`\n\n**Default:** `'-'`\n\nThe Error log file to write to.\n\nUsing ``'-'`` for FILE makes gunicorn log to stderr.\n\n!!! info \"Changed in 19.2\"\n    Log to stderr by default.\n\n### `loglevel`\n\n**Command line:** `--log-level LEVEL`\n\n**Default:** `'info'`\n\nThe granularity of Error log outputs.\n\nValid level names are:\n\n* ``'debug'``\n* ``'info'``\n* ``'warning'``\n* ``'error'``\n* ``'critical'``\n\n### `capture_output`\n\n**Command line:** `--capture-output`\n\n**Default:** `False`\n\nRedirect stdout/stderr to specified file in [errorlog](#errorlog).\n\n!!! info \"Added in 19.6\"\n\n### `logger_class`\n\n**Command line:** `--logger-class STRING`\n\n**Default:** `'gunicorn.glogging.Logger'`\n\nThe logger you want to use to log events in Gunicorn.\n\nThe default class (``gunicorn.glogging.Logger``) handles most\nnormal usages in logging. It provides error and access logging.\n\nYou can provide your own logger by giving Gunicorn a Python path to a\nclass that quacks like ``gunicorn.glogging.Logger``.\n\n### `logconfig`\n\n**Command line:** `--log-config FILE`\n\n**Default:** `None`\n\nThe log config file to use.\nGunicorn uses the standard Python logging module's Configuration\nfile format.\n\n### `logconfig_dict`\n\n**Default:** `{}`\n\nThe log config dictionary to use, using the standard Python\nlogging module's dictionary configuration format. This option\ntakes precedence over the [logconfig](#logconfig) and [logconfig-json](#logconfig_json) options,\nwhich uses the older file configuration format and JSON\nrespectively.\n\nFormat: https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig\n\nFor more context you can look at the default configuration dictionary for logging,\nwhich can be found at ``gunicorn.glogging.CONFIG_DEFAULTS``.\n\n!!! info \"Added in 19.8\"\n\n### `logconfig_json`\n\n**Command line:** `--log-config-json FILE`\n\n**Default:** `None`\n\nThe log config to read config from a JSON file\n\nFormat: https://docs.python.org/3/library/logging.config.html#logging.config.jsonConfig\n\n!!! info \"Added in 20.0\"\n\n### `syslog_addr`\n\n**Command line:** `--log-syslog-to SYSLOG_ADDR`\n\n**Default:**\n\nPlatform-specific:\n\n* macOS: ``'unix:///var/run/syslog'``\n* FreeBSD/DragonFly: ``'unix:///var/run/log'``\n* OpenBSD: ``'unix:///dev/log'``\n* Linux/other: ``'udp://localhost:514'``\n\nAddress to send syslog messages.\n\nAddress is a string of the form:\n\n* ``unix://PATH#TYPE`` : for unix domain socket. ``TYPE`` can be ``stream``\n  for the stream driver or ``dgram`` for the dgram driver.\n  ``stream`` is the default.\n* ``udp://HOST:PORT`` : for UDP sockets\n* ``tcp://HOST:PORT`` : for TCP sockets\n\n### `syslog`\n\n**Command line:** `--log-syslog`\n\n**Default:** `False`\n\nSend *Gunicorn* logs to syslog.\n\n!!! info \"Changed in 19.8\"\n    You can now disable sending access logs by using the\n    disable-redirect-access-to-syslog setting.\n\n### `syslog_prefix`\n\n**Command line:** `--log-syslog-prefix SYSLOG_PREFIX`\n\n**Default:** `None`\n\nMakes Gunicorn use the parameter as program-name in the syslog entries.\n\nAll entries will be prefixed by ``gunicorn.<prefix>``. By default the\nprogram name is the name of the process.\n\n### `syslog_facility`\n\n**Command line:** `--log-syslog-facility SYSLOG_FACILITY`\n\n**Default:** `'user'`\n\nSyslog facility name\n\n### `enable_stdio_inheritance`\n\n**Command line:** `-R`, `--enable-stdio-inheritance`\n\n**Default:** `False`\n\nEnable stdio inheritance.\n\nEnable inheritance for stdio file descriptors in daemon mode.\n\nNote: To disable the Python stdout buffering, you can to set the user\nenvironment variable ``PYTHONUNBUFFERED`` .\n\n### `statsd_host`\n\n**Command line:** `--statsd-host STATSD_ADDR`\n\n**Default:** `None`\n\nThe address of the StatsD server to log to.\n\nAddress is a string of the form:\n\n* ``unix://PATH`` : for a unix domain socket.\n* ``HOST:PORT`` : for a network address\n\n!!! info \"Added in 19.1\"\n\n### `dogstatsd_tags`\n\n**Command line:** `--dogstatsd-tags DOGSTATSD_TAGS`\n\n**Default:** `''`\n\nA comma-delimited list of datadog statsd (dogstatsd) tags to append to\nstatsd metrics. e.g. ``'tag1:value1,tag2:value2'``\n\n!!! info \"Added in 20\"\n\n### `statsd_prefix`\n\n**Command line:** `--statsd-prefix STATSD_PREFIX`\n\n**Default:** `''`\n\nPrefix to use when emitting statsd metrics (a trailing ``.`` is added,\nif not provided).\n\n!!! info \"Added in 19.2\"\n\n### `enable_backlog_metric`\n\n**Command line:** `--enable-backlog-metric`\n\n**Default:** `False`\n\nEnable socket backlog metric (only supported on Linux).\n\nWhen enabled, gunicorn will emit a ``gunicorn.backlog`` histogram metric\nshowing the number of connections waiting in the socket backlog.\n\n## Process Naming\n\n### `proc_name`\n\n**Command line:** `-n STRING`, `--name STRING`\n\n**Default:** `None`\n\nA base to use with setproctitle for process naming.\n\nThis affects things like ``ps`` and ``top``. If you're going to be\nrunning more than one instance of Gunicorn you'll probably want to set a\nname to tell them apart. This requires that you install the setproctitle\nmodule.\n\nIf not set, the *default_proc_name* setting will be used.\n\n### `default_proc_name`\n\n**Default:** `'gunicorn'`\n\nInternal setting that is adjusted for each type of application.\n\n## SSL\n\n### `keyfile`\n\n**Command line:** `--keyfile FILE`\n\n**Default:** `None`\n\nSSL key file\n\n### `certfile`\n\n**Command line:** `--certfile FILE`\n\n**Default:** `None`\n\nSSL certificate file\n\n### `ssl_version`\n\n**Command line:** `--ssl-version`\n\n**Default:** `<_SSLMethod.PROTOCOL_TLS: 2>`\n\nSSL version to use (see stdlib ssl module's).\n\n!!! danger \"Deprecated in 21.0\"\n    The option is deprecated and it is currently ignored. Use [ssl-context](#ssl_context) instead.\n\n============= ============\n--ssl-version Description\n============= ============\nSSLv3         SSLv3 is not-secure and is strongly discouraged.\nSSLv23        Alias for TLS. Deprecated in Python 3.6, use TLS.\nTLS           Negotiate highest possible version between client/server.\n              Can yield SSL. (Python 3.6+)\nTLSv1         TLS 1.0\nTLSv1_1       TLS 1.1 (Python 3.4+)\nTLSv1_2       TLS 1.2 (Python 3.4+)\nTLS_SERVER    Auto-negotiate the highest protocol version like TLS,\n              but only support server-side SSLSocket connections.\n              (Python 3.6+)\n============= ============\n\n!!! info \"Changed in 19.7\"\n    The default value has been changed from ``ssl.PROTOCOL_TLSv1`` to\n    ``ssl.PROTOCOL_SSLv23``.\n\n!!! info \"Changed in 20.0\"\n    This setting now accepts string names based on ``ssl.PROTOCOL_``\n    constants.\n\n!!! info \"Changed in 20.0.1\"\n    The default value has been changed from ``ssl.PROTOCOL_SSLv23`` to\n    ``ssl.PROTOCOL_TLS`` when Python >= 3.6 .\n\n### `cert_reqs`\n\n**Command line:** `--cert-reqs`\n\n**Default:** `<VerifyMode.CERT_NONE: 0>`\n\nWhether client certificate is required (see stdlib ssl module's)\n\n===========  ===========================\n--cert-reqs      Description\n===========  ===========================\n`0`          no client verification\n`1`          ssl.CERT_OPTIONAL\n`2`          ssl.CERT_REQUIRED\n===========  ===========================\n\n### `ca_certs`\n\n**Command line:** `--ca-certs FILE`\n\n**Default:** `None`\n\nCA certificates file\n\n### `suppress_ragged_eofs`\n\n**Command line:** `--suppress-ragged-eofs`\n\n**Default:** `True`\n\nSuppress ragged EOFs (see stdlib ssl module's)\n\n### `do_handshake_on_connect`\n\n**Command line:** `--do-handshake-on-connect`\n\n**Default:** `False`\n\nWhether to perform SSL handshake on socket connect (see stdlib ssl module's)\n\n### `ciphers`\n\n**Command line:** `--ciphers`\n\n**Default:** `None`\n\nSSL Cipher suite to use, in the format of an OpenSSL cipher list.\n\nBy default we use the default cipher list from Python's ``ssl`` module,\nwhich contains ciphers considered strong at the time of each Python\nrelease.\n\nAs a recommended alternative, the Open Web App Security Project (OWASP)\noffers `a vetted set of strong cipher strings rated A+ to C-\n<https://www.owasp.org/index.php/TLS_Cipher_String_Cheat_Sheet>`_.\nOWASP provides details on user-agent compatibility at each security level.\n\nSee the `OpenSSL Cipher List Format Documentation\n<https://www.openssl.org/docs/manmaster/man1/ciphers.html#CIPHER-LIST-FORMAT>`_\nfor details on the format of an OpenSSL cipher list.\n\n## Security\n\n### `limit_request_line`\n\n**Command line:** `--limit-request-line INT`\n\n**Default:** `4094`\n\nThe maximum size of HTTP request line in bytes.\n\nThis parameter is used to limit the allowed size of a client's\nHTTP request-line. Since the request-line consists of the HTTP\nmethod, URI, and protocol version, this directive places a\nrestriction on the length of a request-URI allowed for a request\non the server. A server needs this value to be large enough to\nhold any of its resource names, including any information that\nmight be passed in the query part of a GET request. Value is a number\nfrom 0 (unlimited) to 8190.\n\nThis parameter can be used to prevent any DDOS attack.\n\n### `limit_request_fields`\n\n**Command line:** `--limit-request-fields INT`\n\n**Default:** `100`\n\nLimit the number of HTTP headers fields in a request.\n\nThis parameter is used to limit the number of headers in a request to\nprevent DDOS attack. Used with the *limit_request_field_size* it allows\nmore safety. By default this value is 100 and can't be larger than\n32768.\n\n### `limit_request_field_size`\n\n**Command line:** `--limit-request-field_size INT`\n\n**Default:** `8190`\n\nLimit the allowed size of an HTTP request header field.\n\nValue is a positive number or 0. Setting it to 0 will allow unlimited\nheader field sizes.\n\n!!! warning\n    Setting this parameter to a very high or unlimited value can open\n    up for DDOS attacks.\n\n## Server Hooks\n\n### `on_starting`\n\n**Default:**\n\n```python\ndef on_starting(server):\n    pass\n```\n\nCalled just before the master process is initialized.\n\nThe callable needs to accept a single instance variable for the Arbiter.\n\n### `on_reload`\n\n**Default:**\n\n```python\ndef on_reload(server):\n    pass\n```\n\nCalled to recycle workers during a reload via SIGHUP.\n\nThe callable needs to accept a single instance variable for the Arbiter.\n\n### `when_ready`\n\n**Default:**\n\n```python\ndef when_ready(server):\n    pass\n```\n\nCalled just after the server is started.\n\nThe callable needs to accept a single instance variable for the Arbiter.\n\n### `pre_fork`\n\n**Default:**\n\n```python\ndef pre_fork(server, worker):\n    pass\n```\n\nCalled just before a worker is forked.\n\nThe callable needs to accept two instance variables for the Arbiter and\nnew Worker.\n\n### `post_fork`\n\n**Default:**\n\n```python\ndef post_fork(server, worker):\n    pass\n```\n\nCalled just after a worker has been forked.\n\nThe callable needs to accept two instance variables for the Arbiter and\nnew Worker.\n\n### `post_worker_init`\n\n**Default:**\n\n```python\ndef post_worker_init(worker):\n    pass\n```\n\nCalled just after a worker has initialized the application.\n\nThe callable needs to accept one instance variable for the initialized\nWorker.\n\n### `worker_int`\n\n**Default:**\n\n```python\ndef worker_int(worker):\n    pass\n```\n\nCalled just after a worker exited on SIGINT or SIGQUIT.\n\nThe callable needs to accept one instance variable for the initialized\nWorker.\n\n### `worker_abort`\n\n**Default:**\n\n```python\ndef worker_abort(worker):\n    pass\n```\n\nCalled when a worker received the SIGABRT signal.\n\nThis call generally happens on timeout.\n\nThe callable needs to accept one instance variable for the initialized\nWorker.\n\n### `pre_exec`\n\n**Default:**\n\n```python\ndef pre_exec(server):\n    pass\n```\n\nCalled just before a new master process is forked.\n\nThe callable needs to accept a single instance variable for the Arbiter.\n\n### `pre_request`\n\n**Default:**\n\n```python\ndef pre_request(worker, req):\n    worker.log.debug(\"%s %s\", req.method, req.path)\n```\n\nCalled just before a worker processes the request.\n\nThe callable needs to accept two instance variables for the Worker and\nthe Request.\n\n### `post_request`\n\n**Default:**\n\n```python\ndef post_request(worker, req, environ, resp):\n    pass\n```\n\nCalled after a worker processes the request.\n\nThe callable needs to accept two instance variables for the Worker and\nthe Request. If a third parameter is defined it will be passed the\nenvironment. If a fourth parameter is defined it will be passed the Response.\n\n### `child_exit`\n\n**Default:**\n\n```python\ndef child_exit(server, worker):\n    pass\n```\n\nCalled just after a worker has been exited, in the master process.\n\nThe callable needs to accept two instance variables for the Arbiter and\nthe just-exited Worker.\n\n!!! info \"Added in 19.7\"\n\n### `worker_exit`\n\n**Default:**\n\n```python\ndef worker_exit(server, worker):\n    pass\n```\n\nCalled just after a worker has been exited, in the worker process.\n\nThe callable needs to accept two instance variables for the Arbiter and\nthe just-exited Worker.\n\n### `nworkers_changed`\n\n**Default:**\n\n```python\ndef nworkers_changed(server, new_value, old_value):\n    pass\n```\n\nCalled just after *num_workers* has been changed.\n\nThe callable needs to accept an instance variable of the Arbiter and\ntwo integers of number of workers after and before change.\n\nIf the number of workers is set for the first time, *old_value* would\nbe ``None``.\n\n### `on_exit`\n\n**Default:**\n\n```python\ndef on_exit(server):\n    pass\n```\n\nCalled just before exiting Gunicorn.\n\nThe callable needs to accept a single instance variable for the Arbiter.\n\n### `ssl_context`\n\n**Default:**\n\n```python\ndef ssl_context(config, default_ssl_context_factory):\n    return default_ssl_context_factory()\n```\n\nCalled when SSLContext is needed.\n\nAllows customizing SSL context.\n\nThe callable needs to accept an instance variable for the Config and\na factory function that returns default SSLContext which is initialized\nwith certificates, private key, cert_reqs, and ciphers according to\nconfig and can be further customized by the callable.\nThe callable needs to return SSLContext object.\n\nFollowing example shows a configuration file that sets the minimum TLS version to 1.3:\n\n```python\ndef ssl_context(conf, default_ssl_context_factory):\n    import ssl\n    context = default_ssl_context_factory()\n    context.minimum_version = ssl.TLSVersion.TLSv1_3\n    return context\n```\n\n!!! info \"Added in 21.0\"\n\n## Server Mechanics\n\n### `preload_app`\n\n**Command line:** `--preload`\n\n**Default:** `False`\n\nLoad application code before the worker processes are forked.\n\nBy preloading an application you can save some RAM resources as well as\nspeed up server boot times. Although, if you defer application loading\nto each worker process, you can reload your application code easily by\nrestarting workers.\n\n### `sendfile`\n\n**Command line:** `--no-sendfile`\n\n**Default:** `None`\n\nDisables the use of ``sendfile()``.\n\nIf not set, the value of the ``SENDFILE`` environment variable is used\nto enable or disable its usage.\n\n!!! info \"Added in 19.2\"\n\n!!! info \"Changed in 19.4\"\n    Swapped ``--sendfile`` with ``--no-sendfile`` to actually allow\n    disabling.\n\n!!! info \"Changed in 19.6\"\n    added support for the ``SENDFILE`` environment variable\n\n### `reuse_port`\n\n**Command line:** `--reuse-port`\n\n**Default:** `False`\n\nSet the ``SO_REUSEPORT`` flag on the listening socket.\n\n!!! info \"Added in 19.8\"\n\n### `chdir`\n\n**Command line:** `--chdir`\n\n**Default:**\n\n``'.'``\n\nChange directory to specified directory before loading apps.\n\n### `daemon`\n\n**Command line:** `-D`, `--daemon`\n\n**Default:** `False`\n\nDaemonize the Gunicorn process.\n\nDetaches the server from the controlling terminal and enters the\nbackground.\n\n### `raw_env`\n\n**Command line:** `-e ENV`, `--env ENV`\n\n**Default:** `[]`\n\nSet environment variables in the execution environment.\n\nShould be a list of strings in the ``key=value`` format.\n\nFor example on the command line:\n\n```console\n$ gunicorn -b 127.0.0.1:8000 --env FOO=1 test:app\n```\n\nOr in the configuration file:\n\n```python\nraw_env = [\"FOO=1\"]\n```\n\n### `pidfile`\n\n**Command line:** `-p FILE`, `--pid FILE`\n\n**Default:** `None`\n\nA filename to use for the PID file.\n\nIf not set, no PID file will be written.\n\n### `worker_tmp_dir`\n\n**Command line:** `--worker-tmp-dir DIR`\n\n**Default:** `None`\n\nA directory to use for the worker heartbeat temporary file.\n\nIf not set, the default temporary directory will be used.\n\n!!! note\n    The current heartbeat system involves calling ``os.fchmod`` on\n    temporary file handlers and may block a worker for arbitrary time\n    if the directory is on a disk-backed filesystem.\n\n    See [blocking-os-fchmod](#blocking_os_fchmod) for more detailed information\n    and a solution for avoiding this problem.\n\n### `user`\n\n**Command line:** `-u USER`, `--user USER`\n\n**Default:**\n\n``os.geteuid()``\n\nSwitch worker processes to run as this user.\n\nA valid user id (as an integer) or the name of a user that can be\nretrieved with a call to ``pwd.getpwnam(value)`` or ``None`` to not\nchange the worker process user.\n\n### `group`\n\n**Command line:** `-g GROUP`, `--group GROUP`\n\n**Default:**\n\n``os.getegid()``\n\nSwitch worker process to run as this group.\n\nA valid group id (as an integer) or the name of a user that can be\nretrieved with a call to ``grp.getgrnam(value)`` or ``None`` to not\nchange the worker processes group.\n\n### `umask`\n\n**Command line:** `-m INT`, `--umask INT`\n\n**Default:** `0`\n\nA bit mask for the file mode on files written by Gunicorn.\n\nNote that this affects unix socket permissions.\n\nA valid value for the ``os.umask(mode)`` call or a string compatible\nwith ``int(value, 0)`` (``0`` means Python guesses the base, so values\nlike ``0``, ``0xFF``, ``0022`` are valid for decimal, hex, and octal\nrepresentations)\n\n### `initgroups`\n\n**Command line:** `--initgroups`\n\n**Default:** `False`\n\nIf true, set the worker process's group access list with all of the\ngroups of which the specified username is a member, plus the specified\ngroup id.\n\n!!! info \"Added in 19.7\"\n\n### `tmp_upload_dir`\n\n**Default:** `None`\n\nDirectory to store temporary request data as they are read.\n\nThis may disappear in the near future.\n\nThis path should be writable by the process permissions set for Gunicorn\nworkers. If not specified, Gunicorn will choose a system generated\ntemporary directory.\n\n### `secure_scheme_headers`\n\n**Default:** `{'X-FORWARDED-PROTOCOL': 'ssl', 'X-FORWARDED-PROTO': 'https', 'X-FORWARDED-SSL': 'on'}`\n\nA dictionary containing headers and values that the front-end proxy\nuses to indicate HTTPS requests. If the source IP is permitted by\n[forwarded-allow-ips](#forwarded_allow_ips) (below), *and* at least one request header matches\na key-value pair listed in this dictionary, then Gunicorn will set\n``wsgi.url_scheme`` to ``https``, so your application can tell that the\nrequest is secure.\n\nIf the other headers listed in this dictionary are not present in the request, they will be ignored,\nbut if the other headers are present and do not match the provided values, then\nthe request will fail to parse. See the note below for more detailed examples of this behaviour.\n\nThe dictionary should map upper-case header names to exact string\nvalues. The value comparisons are case-sensitive, unlike the header\nnames, so make sure they're exactly what your front-end proxy sends\nwhen handling HTTPS requests.\n\nIt is important that your front-end proxy configuration ensures that\nthe headers defined here can not be passed directly from the client.\n\n### `forwarded_allow_ips`\n\n**Command line:** `--forwarded-allow-ips STRING`\n\n**Default:** `'127.0.0.1,::1'`\n\nFront-end's IP addresses or networks from which allowed to handle\nset secure headers. (comma separated).\n\nSupports both individual IP addresses (e.g., ``192.168.1.1``) and\nCIDR networks (e.g., ``192.168.0.0/16``).\n\nSet to ``*`` to disable checking of front-end IPs. This is useful for setups\nwhere you don't know in advance the IP address of front-end, but\ninstead have ensured via other means that only your\nauthorized front-ends can access Gunicorn.\n\nBy default, the value of the ``FORWARDED_ALLOW_IPS`` environment\nvariable. If it is not defined, the default is ``\"127.0.0.1,::1\"``.\n\n!!! note\n    This option does not affect UNIX socket connections. Connections not associated with\n    an IP address are treated as allowed, unconditionally.\n\n!!! note\n    The interplay between the request headers, the value of ``forwarded_allow_ips``, and the value of\n    ``secure_scheme_headers`` is complex. Various scenarios are documented below to further elaborate.\n    In each case, we have a request from the remote address 134.213.44.18, and the default value of\n    ``secure_scheme_headers``:\n\n    ```python\n    secure_scheme_headers = {\n        'X-FORWARDED-PROTOCOL': 'ssl',\n        'X-FORWARDED-PROTO': 'https',\n        'X-FORWARDED-SSL': 'on'\n    }\n    ```\n\n    +---------------------+----------------------------+-----------------------------+-------------------------+\n    | forwarded-allow-ips | Secure Request Headers     | Result                      | Explanation             |\n    +=====================+============================+=============================+=========================+\n    | `\"127.0.0.1\"`       | `X-Forwarded-Proto: https` | `wsgi.url_scheme = \"http\"`  | IP address was not      |\n    |                     |                            |                             | allowed                 |\n    +---------------------+----------------------------+-----------------------------+-------------------------+\n    |                     |                            |                             | IP address allowed, but |\n    | `\"*\"`               | `<none>`                   | `wsgi.url_scheme = \"http\"`  | no secure headers       |\n    |                     |                            |                             | provided                |\n    +---------------------+----------------------------+-----------------------------+-------------------------+\n    | `\"*\"`               | `X-Forwarded-Proto: https` | `wsgi.url_scheme = \"https\"` | IP address allowed, one |\n    |                     |                            |                             | request header matched  |\n    +---------------------+----------------------------+-----------------------------+-------------------------+\n    |                     |                            |                             | IP address allowed, but |\n    | `\"134.213.44.18\"`   | `X-Forwarded-Ssl: on`      | `InvalidSchemeHeaders()`    | the two secure headers  |\n    |                     | `X-Forwarded-Proto: http`  | raised                      | disagreed on if HTTPS   |\n    |                     |                            |                             | was used                |\n    +---------------------+----------------------------+-----------------------------+-------------------------+\n\n### `pythonpath`\n\n**Command line:** `--pythonpath STRING`\n\n**Default:** `None`\n\nA comma-separated list of directories to add to the Python path.\n\ne.g.\n``'/home/djangoprojects/myproject,/home/python/mylibrary'``.\n\n### `paste`\n\n**Command line:** `--paste STRING`, `--paster STRING`\n\n**Default:** `None`\n\nLoad a PasteDeploy config file. The argument may contain a ``#``\nsymbol followed by the name of an app section from the config file,\ne.g. ``production.ini#admin``.\n\nAt this time, using alternate server blocks is not supported. Use the\ncommand line arguments to control server configuration instead.\n\n### `proxy_protocol`\n\n**Command line:** `--proxy-protocol MODE`\n\n**Default:** `'off'`\n\nEnable PROXY protocol support.\n\nAllow using HTTP and PROXY protocol together. It may be useful for work\nwith stunnel as HTTPS frontend and Gunicorn as HTTP server, or with\nHAProxy.\n\nAccepted values:\n\n* ``off`` - Disabled (default)\n* ``v1`` - PROXY protocol v1 only (text format)\n* ``v2`` - PROXY protocol v2 only (binary format)\n* ``auto`` - Auto-detect v1 or v2\n\nUsing ``--proxy-protocol`` without a value is equivalent to ``auto``.\n\nPROXY protocol v1: http://haproxy.1wt.eu/download/1.5/doc/proxy-protocol.txt\nPROXY protocol v2: https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt\n\nExample for stunnel config::\n\n    [https]\n    protocol = proxy\n    accept  = 443\n    connect = 80\n    cert = /etc/ssl/certs/stunnel.pem\n    key = /etc/ssl/certs/stunnel.key\n\n!!! info \"Changed in 24.1.0\"\n    Extended to support version selection (v1, v2, auto).\n\n### `proxy_allow_ips`\n\n**Command line:** `--proxy-allow-from`\n\n**Default:** `'127.0.0.1,::1'`\n\nFront-end's IP addresses or networks from which allowed accept\nproxy requests (comma separated).\n\nSupports both individual IP addresses (e.g., ``192.168.1.1``) and\nCIDR networks (e.g., ``192.168.0.0/16``).\n\nSet to ``*`` to disable checking of front-end IPs. This is useful for setups\nwhere you don't know in advance the IP address of front-end, but\ninstead have ensured via other means that only your\nauthorized front-ends can access Gunicorn.\n\n!!! note\n    This option does not affect UNIX socket connections. Connections not associated with\n    an IP address are treated as allowed, unconditionally.\n\n### `protocol`\n\n**Command line:** `--protocol STRING`\n\n**Default:** `'http'`\n\nThe protocol for incoming connections.\n\n* ``http`` - Standard HTTP/1.x (default)\n* ``uwsgi`` - uWSGI binary protocol (for nginx uwsgi_pass)\n\nWhen using the uWSGI protocol, Gunicorn can receive requests from\nnginx using the uwsgi_pass directive::\n\n    upstream gunicorn {\n        server 127.0.0.1:8000;\n    }\n    location / {\n        uwsgi_pass gunicorn;\n        include uwsgi_params;\n    }\n\n### `uwsgi_allow_ips`\n\n**Command line:** `--uwsgi-allow-from`\n\n**Default:** `'127.0.0.1,::1'`\n\nIPs allowed to send uWSGI protocol requests (comma separated).\n\nSet to ``*`` to allow all IPs. This is useful for setups where you\ndon't know in advance the IP address of front-end, but instead have\nensured via other means that only your authorized front-ends can\naccess Gunicorn.\n\n!!! note\n    This option does not affect UNIX socket connections. Connections not associated with\n    an IP address are treated as allowed, unconditionally.\n\n### `raw_paste_global_conf`\n\n**Command line:** `--paste-global CONF`\n\n**Default:** `[]`\n\nSet a PasteDeploy global config variable in ``key=value`` form.\n\nThe option can be specified multiple times.\n\nThe variables are passed to the PasteDeploy entrypoint. Example::\n\n    $ gunicorn -b 127.0.0.1:8000 --paste development.ini --paste-global FOO=1 --paste-global BAR=2\n\n!!! info \"Added in 19.7\"\n\n### `permit_obsolete_folding`\n\n**Command line:** `--permit-obsolete-folding`\n\n**Default:** `False`\n\nPermit requests employing obsolete HTTP line folding mechanism\n\nThe folding mechanism was deprecated by rfc7230 Section 3.2.4 and will not be\n employed in HTTP request headers from standards-compliant HTTP clients.\n\nThis option is provided to diagnose backwards-incompatible changes.\nUse with care and only if necessary. Temporary; the precise effect of this option may\nchange in a future version, or it may be removed altogether.\n\n!!! info \"Added in 23.0.0\"\n\n### `strip_header_spaces`\n\n**Command line:** `--strip-header-spaces`\n\n**Default:** `False`\n\nStrip spaces present between the header name and the the ``:``.\n\nThis is known to induce vulnerabilities and is not compliant with the HTTP/1.1 standard.\nSee https://portswigger.net/research/http-desync-attacks-request-smuggling-reborn.\n\nUse with care and only if necessary. Deprecated; scheduled for removal in 25.0.0\n\n!!! info \"Added in 20.0.1\"\n\n### `permit_unconventional_http_method`\n\n**Command line:** `--permit-unconventional-http-method`\n\n**Default:** `False`\n\nPermit HTTP methods not matching conventions, such as IANA registration guidelines\n\nThis permits request methods of length less than 3 or more than 20,\nmethods with lowercase characters or methods containing the # character.\nHTTP methods are case sensitive by definition, and merely uppercase by convention.\n\nIf unset, Gunicorn will apply nonstandard restrictions and cause 400 response status\nin cases where otherwise 501 status is expected. While this option does modify that\nbehaviour, it should not be depended upon to guarantee standards-compliant behaviour.\nRather, it is provided temporarily, to assist in diagnosing backwards-incompatible\nchanges around the incomplete application of those restrictions.\n\nUse with care and only if necessary. Temporary; scheduled for removal in 24.0.0\n\n!!! info \"Added in 22.0.0\"\n\n### `permit_unconventional_http_version`\n\n**Command line:** `--permit-unconventional-http-version`\n\n**Default:** `False`\n\nPermit HTTP version not matching conventions of 2023\n\nThis disables the refusal of likely malformed request lines.\nIt is unusual to specify HTTP 1 versions other than 1.0 and 1.1.\n\nThis option is provided to diagnose backwards-incompatible changes.\nUse with care and only if necessary. Temporary; the precise effect of this option may\nchange in a future version, or it may be removed altogether.\n\n!!! info \"Added in 22.0.0\"\n\n### `casefold_http_method`\n\n**Command line:** `--casefold-http-method`\n\n**Default:** `False`\n\nTransform received HTTP methods to uppercase\n\nHTTP methods are case sensitive by definition, and merely uppercase by convention.\n\nThis option is provided because previous versions of gunicorn defaulted to this behaviour.\n\nUse with care and only if necessary. Deprecated; scheduled for removal in 24.0.0\n\n!!! info \"Added in 22.0.0\"\n\n### `forwarder_headers`\n\n**Command line:** `--forwarder-headers`\n\n**Default:** `'SCRIPT_NAME,PATH_INFO'`\n\nA list containing upper-case header field names that the front-end proxy\n(see [forwarded-allow-ips](#forwarded_allow_ips)) sets, to be used in WSGI environment.\n\nThis option has no effect for headers not present in the request.\n\nThis option can be used to transfer ``SCRIPT_NAME``, ``PATH_INFO``\nand ``REMOTE_USER``.\n\nIt is important that your front-end proxy configuration ensures that\nthe headers defined here can not be passed directly from the client.\n\n### `header_map`\n\n**Command line:** `--header-map`\n\n**Default:** `'drop'`\n\nConfigure how header field names are mapped into environ\n\nHeaders containing underscores are permitted by RFC9110,\nbut gunicorn joining headers of different names into\nthe same environment variable will dangerously confuse applications as to which is which.\n\nThe safe default ``drop`` is to silently drop headers that cannot be unambiguously mapped.\nThe value ``refuse`` will return an error if a request contains *any* such header.\nThe value ``dangerous`` matches the previous, not advisable, behaviour of mapping different\nheader field names into the same environ name.\n\nIf the source is permitted as explained in [forwarded-allow-ips](#forwarded_allow_ips), *and* the header name is\npresent in [forwarder-headers](#forwarder_headers), the header is mapped into environment regardless of\nthe state of this setting.\n\nUse with care and only if necessary and after considering if your problem could\ninstead be solved by specifically renaming or rewriting only the intended headers\non a proxy in front of Gunicorn.\n\n!!! info \"Added in 22.0.0\"\n\n### `root_path`\n\n**Command line:** `--root-path STRING`\n\n**Default:** `''`\n\nThe root path for ASGI applications.\n\nThis is used to set the ``root_path`` in the ASGI scope, which\nallows applications to know their mount point when behind a\nreverse proxy.\n\nFor example, if your application is mounted at ``/api``, set\nthis to ``/api``.\n\n!!! info \"Added in 24.0.0\"\n\n## Server Socket\n\n### `bind`\n\n**Command line:** `-b ADDRESS`, `--bind ADDRESS`\n\n**Default:** `['127.0.0.1:8000']`\n\nThe socket to bind.\n\nA string of the form: ``HOST``, ``HOST:PORT``, ``unix:PATH``,\n``fd://FD``. An IP is a valid ``HOST``.\n\n!!! info \"Changed in 20.0\"\n    Support for ``fd://FD`` got added.\n\nMultiple addresses can be bound. ex.::\n\n    $ gunicorn -b 127.0.0.1:8000 -b [::1]:8000 test:app\n\nwill bind the `test:app` application on localhost both on ipv6\nand ipv4 interfaces.\n\nIf the ``PORT`` environment variable is defined, the default\nis ``['0.0.0.0:$PORT']``. If it is not defined, the default\nis ``['127.0.0.1:8000']``.\n\n### `backlog`\n\n**Command line:** `--backlog INT`\n\n**Default:** `2048`\n\nThe maximum number of pending connections.\n\nThis refers to the number of clients that can be waiting to be served.\nExceeding this number results in the client getting an error when\nattempting to connect. It should only affect servers under significant\nload.\n\nMust be a positive integer. Generally set in the 64-2048 range.\n\n## Worker Processes\n\n### `workers`\n\n**Command line:** `-w INT`, `--workers INT`\n\n**Default:** `1`\n\nThe number of worker processes for handling requests.\n\nA positive integer generally in the ``2-4 x $(NUM_CORES)`` range.\nYou'll want to vary this a bit to find the best for your particular\napplication's work load.\n\nBy default, the value of the ``WEB_CONCURRENCY`` environment variable,\nwhich is set by some Platform-as-a-Service providers such as Heroku. If\nit is not defined, the default is ``1``.\n\n### `worker_class`\n\n**Command line:** `-k STRING`, `--worker-class STRING`\n\n**Default:** `'sync'`\n\nThe type of workers to use.\n\nThe default class (``sync``) should handle most \"normal\" types of\nworkloads. You'll want to read :doc:`design` for information on when\nyou might want to choose one of the other worker classes. Required\nlibraries may be installed using setuptools' ``extras_require`` feature.\n\nA string referring to one of the following bundled classes:\n\n* ``sync``\n* ``eventlet`` - **DEPRECATED: will be removed in 26.0**. Requires eventlet >= 0.40.3\n* ``gevent``   - Requires gevent >= 24.10.1 (or install it via\n  ``pip install gunicorn[gevent]``)\n* ``tornado``  - Requires tornado >= 6.5.0 (or install it via\n  ``pip install gunicorn[tornado]``)\n* ``gthread``  - Python 2 requires the futures package to be installed\n  (or install it via ``pip install gunicorn[gthread]``)\n\nOptionally, you can provide your own worker by giving Gunicorn a\nPython path to a subclass of ``gunicorn.workers.base.Worker``.\nThis alternative syntax will load the gevent class:\n``gunicorn.workers.ggevent.GeventWorker``.\n\n### `threads`\n\n**Command line:** `--threads INT`\n\n**Default:** `1`\n\nThe number of worker threads for handling requests.\n\nRun each worker with the specified number of threads.\n\nA positive integer generally in the ``2-4 x $(NUM_CORES)`` range.\nYou'll want to vary this a bit to find the best for your particular\napplication's work load.\n\nIf it is not defined, the default is ``1``.\n\nThis setting only affects the Gthread worker type.\n\n!!! note\n    If you try to use the ``sync`` worker type and set the ``threads``\n    setting to more than 1, the ``gthread`` worker type will be used\n    instead.\n\n### `worker_connections`\n\n**Command line:** `--worker-connections INT`\n\n**Default:** `1000`\n\nThe maximum number of simultaneous clients.\n\nThis setting only affects the ``gthread``, ``eventlet`` and ``gevent`` worker types.\n\n### `max_requests`\n\n**Command line:** `--max-requests INT`\n\n**Default:** `0`\n\nThe maximum number of requests a worker will process before restarting.\n\nAny value greater than zero will limit the number of requests a worker\nwill process before automatically restarting. This is a simple method\nto help limit the damage of memory leaks.\n\nIf this is set to zero (the default) then the automatic worker\nrestarts are disabled.\n\n### `max_requests_jitter`\n\n**Command line:** `--max-requests-jitter INT`\n\n**Default:** `0`\n\nThe maximum jitter to add to the *max_requests* setting.\n\nThe jitter causes the restart per worker to be randomized by\n``randint(0, max_requests_jitter)``. This is intended to stagger worker\nrestarts to avoid all workers restarting at the same time.\n\n!!! info \"Added in 19.2\"\n\n### `timeout`\n\n**Command line:** `-t INT`, `--timeout INT`\n\n**Default:** `30`\n\nWorkers silent for more than this many seconds are killed and restarted.\n\nValue is a positive number or 0. Setting it to 0 has the effect of\ninfinite timeouts by disabling timeouts for all workers entirely.\n\nGenerally, the default of thirty seconds should suffice. Only set this\nnoticeably higher if you're sure of the repercussions for sync workers.\nFor the non sync workers it just means that the worker process is still\ncommunicating and is not tied to the length of time required to handle a\nsingle request.\n\n### `graceful_timeout`\n\n**Command line:** `--graceful-timeout INT`\n\n**Default:** `30`\n\nTimeout for graceful workers restart in seconds.\n\nAfter receiving a restart signal, workers have this much time to finish\nserving requests. Workers still alive after the timeout (starting from\nthe receipt of the restart signal) are force killed.\n\n### `keepalive`\n\n**Command line:** `--keep-alive INT`\n\n**Default:** `2`\n\nThe number of seconds to wait for requests on a Keep-Alive connection.\n\nGenerally set in the 1-5 seconds range for servers with direct connection\nto the client (e.g. when you don't have separate load balancer). When\nGunicorn is deployed behind a load balancer, it often makes sense to\nset this to a higher value.\n\n!!! note\n    ``sync`` worker does not support persistent connections and will\n    ignore this option.\n\n### `asgi_loop`\n\n**Command line:** `--asgi-loop STRING`\n\n**Default:** `'auto'`\n\nEvent loop implementation for ASGI workers.\n\n- auto: Use uvloop if available, otherwise asyncio\n- asyncio: Use Python's built-in asyncio event loop\n- uvloop: Use uvloop (must be installed separately)\n\nThis setting only affects the ``asgi`` worker type.\n\nuvloop typically provides better performance but requires\ninstalling the uvloop package.\n\n!!! info \"Added in 24.0.0\"\n\n### `asgi_lifespan`\n\n**Command line:** `--asgi-lifespan STRING`\n\n**Default:** `'auto'`\n\nControl ASGI lifespan protocol handling.\n\n- auto: Detect if app supports lifespan, enable if so\n- on: Always run lifespan protocol (fail if unsupported)\n- off: Never run lifespan protocol\n\nThe lifespan protocol allows ASGI applications to run code at\nstartup and shutdown. This is essential for frameworks like\nFastAPI that need to initialize database connections, caches,\nor other resources.\n\nThis setting only affects the ``asgi`` worker type.\n\n!!! info \"Added in 24.0.0\"\n\n### `asgi_disconnect_grace_period`\n\n**Command line:** `--asgi-disconnect-grace-period INT`\n\n**Default:** `3`\n\nGrace period (seconds) for ASGI apps to handle client disconnects.\n\nWhen a client disconnects, the ASGI app receives an http.disconnect\nmessage and has this many seconds to clean up resources (like database\nconnections) before the request task is cancelled.\n\nSet to 0 to cancel immediately (not recommended for apps with async\ndatabase connections). Apps with long-running database operations may\nneed to increase this value.\n\nThis setting only affects the ``asgi`` worker type.\n\n!!! info \"Added in 25.0.0\"\n"
  },
  {
    "path": "docs/content/run.md",
    "content": "# Running Gunicorn\n\nYou can run Gunicorn directly from the command line or integrate it with\npopular frameworks like Django, Pyramid, or TurboGears. For deployment\npatterns see the [deployment guide](deploy.md).\n\n## Commands\n\nAfter installation you have access to the `gunicorn` executable.\n\n<span id=\"gunicorn-cmd\"></span>\n### `gunicorn`\n\nBasic usage:\n\n```bash\ngunicorn [OPTIONS] [WSGI_APP]\n```\n\n`WSGI_APP` follows the pattern `MODULE_NAME:VARIABLE_NAME`. The module can be a\nfull dotted path. The variable refers to a WSGI callable defined in that\nmodule.\n\n!!! info \"Changed in 20.1.0\"\n    `WSGI_APP` can be omitted when defined in a [configuration file](configure.md).\n\n\n\nExample test application:\n\n```python\ndef app(environ, start_response):\n    \"\"\"Simplest possible application object\"\"\"\n    data = b\"Hello, World!\\n\"\n    status = \"200 OK\"\n    response_headers = [\n        (\"Content-type\", \"text/plain\"),\n        (\"Content-Length\", str(len(data.md)))\n    ]\n    start_response(status, response_headers)\n    return iter([data])\n```\n\nRun it with:\n\n```bash\ngunicorn --workers=2 test:app\n```\n\nYou can also expose a factory function that returns the application:\n\n```python\ndef create_app():\n    app = FrameworkApp()\n    ...\n    return app\n```\n\n```bash\ngunicorn --workers=2 'test:create_app()'\n```\n\nPassing positional and keyword arguments is supported but prefer\nconfiguration files or environment variables for anything beyond quick tests.\n\n#### Commonly used arguments\n\n- `-c CONFIG`, `--config CONFIG` &mdash; configuration file (`PATH`, `file:PATH`, or\n  `python:MODULE_NAME`).\n- `-b BIND`, `--bind BIND` &mdash; socket to bind (host, host:port, `fd://FD`,\n  or `unix:PATH`).\n- `-w WORKERS`, `--workers WORKERS` &mdash; number of worker processes, typically\n  two to four per CPU core. See the [FAQ](faq.md) for tuning tips.\n- `-k WORKERCLASS`, `--worker-class WORKERCLASS` &mdash; worker type (`sync`,\n  `gevent`, `tornado`, `gthread`). Read the\n  [settings entry](reference/settings.md#worker_class) before switching classes.\n- `-n APP_NAME`, `--name APP_NAME` &mdash; set the process name (requires\n  [`setproctitle`](https://pypi.python.org/pypi/setproctitle)).\n\nYou can pass any setting via the environment variable\n`GUNICORN_CMD_ARGS`. See the [configuration guide](configure.md) and\n[settings reference](reference/settings.md) for details.\n\n## Integration\n\nGunicorn integrates cleanly with Django and Paste Deploy applications.\n\n### Django\n\nGunicorn looks for a WSGI callable named `application`. A typical invocation is:\n\n```bash\ngunicorn myproject.wsgi\n```\n\n!!! note\n    Ensure your project is on `PYTHONPATH`. The easiest way is to run this command\n    from the directory containing `manage.py`.\n\n\n\nSet environment variables with `--env` and add your project to `PYTHONPATH`\nif needed:\n\n```bash\ngunicorn --env DJANGO_SETTINGS_MODULE=myproject.settings myproject.wsgi\n```\n\nSee [`raw_env`](reference/settings.md#raw_env) and [`pythonpath`](reference/settings.md#pythonpath) for\nmore options.\n\n### Paste Deployment\n\nFrameworks such as Pyramid and TurboGears often rely on Paste Deployment\nconfiguration. You can use Gunicorn in two ways.\n\n#### As a Paste server runner\n\nLet your framework command (for example `pserve` or `gearbox`) load Gunicorn by\nconfiguring it as the server:\n\n```ini\n[server:main]\nuse = egg:gunicorn#main\nhost = 127.0.0.1\nport = 8080\nworkers = 3\n```\n\nThis approach is quick to set up but Gunicorn cannot control how the\napplication loads. Options like [`reload`](reference/settings.md#reload) will be ignored and\nhot upgrades are unavailable. Features such as daemon mode may conflict with\nwhat your framework already provides. Prefer running those features through the\nframework (for example `pserve --reload`). Advanced configuration is still\npossible by pointing the `config` key at a Gunicorn configuration file.\n\n#### Using Gunicorn's Paste support\n\nUse the [`paste`](reference/settings.md#paste) option to load a Paste configuration directly\nwith the Gunicorn CLI. This unlocks Gunicorn's reloader and hot code upgrades,\nwhile still letting Paste define the application object.\n\n```bash\ngunicorn --paste development.ini -b :8080 --chdir /path/to/project\n```\n\nSelect a different application section by appending the name:\n\n```bash\ngunicorn --paste development.ini#admin -b :8080 --chdir /path/to/project\n```\n\nIn both modes Gunicorn will honor any Paste `loggers` configuration unless you\noverride it with Gunicorn-specific [logging settings](reference/settings.md#logging).\n"
  },
  {
    "path": "docs/content/signals.md",
    "content": "<span id=\"signals\"></span>\n# Signal Handling\n\nA quick reference to the signals handled by Gunicorn. This includes the signals\nused internally to coordinate with worker processes.\n\n## Master process\n\n- `QUIT`, `INT` &mdash; quick shutdown.\n- `TERM` &mdash; graceful shutdown; waits for workers to finish requests up to\n  [`graceful_timeout`](reference/settings.md#graceful_timeout).\n- `HUP` &mdash; reload configuration, spawn new workers, and gracefully stop old\n  ones. If the app is not preloaded (see [`preload_app`](reference/settings.md#preload_app))\n  the application code is reloaded too.\n- `TTIN` &mdash; increase worker count by one.\n- `TTOU` &mdash; decrease worker count by one.\n- `USR1` &mdash; reopen log files.\n- `USR2` &mdash; perform a binary upgrade. Send `TERM` to the old master afterwards\n  to stop it. This also reloads preloaded applications (see\n  [binary upgrades](#binary-upgrade)).\n- `WINCH` &mdash; gracefully stop workers when Gunicorn runs as a daemon.\n\n## Worker process\n\nWorkers rarely need direct signalling—if the master stays alive it will respawn\nworkers automatically.\n\n- `QUIT`, `INT` &mdash; quick shutdown.\n- `TERM` &mdash; graceful shutdown.\n- `USR1` &mdash; reopen log files.\n\n## Reload the configuration\n\nUse `HUP` to reload Gunicorn on the fly:\n\n```text\n2013-06-29 06:26:55 [20682] [INFO] Handling signal: hup\n2013-06-29 06:26:55 [20682] [INFO] Hang up: Master\n2013-06-29 06:26:55 [20703] [INFO] Booting worker with pid: 20703\n2013-06-29 06:26:55 [20702] [INFO] Booting worker with pid: 20702\n2013-06-29 06:26:55 [20688] [INFO] Worker exiting (pid: 20688)\n2013-06-29 06:26:55 [20687] [INFO] Worker exiting (pid: 20687)\n2013-06-29 06:26:55 [20689] [INFO] Worker exiting (pid: 20689)\n2013-06-29 06:26:55 [20704] [INFO] Booting worker with pid: 20704\n```\n\nGunicorn reloads its settings, starts new workers, and gracefully shuts down the\nprevious ones. If the app is not preloaded it reloads the application module as\nwell.\n\n<span id=\"binary-upgrade\"></span>\n## Upgrading to a new binary on the fly\n\n!!! info \"Changed in 19.6.0\"\n    PID files now follow the pattern `<name>.pid.2` instead of `<name>.pid.oldbin`.\n\n\n\nYou can replace the Gunicorn binary without downtime. Incoming requests remain\nserved and preloaded applications reload.\n\n1. Replace the old binary and send `USR2` to the master. Gunicorn starts a new\n   master whose PID file ends with `.2` and spawns new workers.\n\n   ```text\n   PID USER      PR  NI  VIRT  RES  SHR S  %CPU %MEM    TIME+  COMMAND\n   20844 benoitc   20   0 54808  11m 3352 S   0.0  0.1   0:00.36 gunicorn: master [test:app]\n   20849 benoitc   20   0 54808 9.9m 1500 S   0.0  0.1   0:00.02 gunicorn: worker [test:app]\n   20850 benoitc   20   0 54808 9.9m 1500 S   0.0  0.1   0:00.01 gunicorn: worker [test:app]\n   20851 benoitc   20   0 54808 9.9m 1500 S   0.0  0.1   0:00.01 gunicorn: worker [test:app]\n   20854 benoitc   20   0 55748  12m 3348 S   0.0  0.2   0:00.35 gunicorn: master [test:app]\n   20859 benoitc   20   0 55748  11m 1500 S   0.0  0.1   0:00.01 gunicorn: worker [test:app]\n   20860 benoitc   20   0 55748  11m 1500 S   0.0  0.1   0:00.00 gunicorn: worker [test:app]\n   20861 benoitc   20   0 55748  11m 1500 S   0.0  0.1   0:00.01 gunicorn: worker [test:app]\n   ```\n\n2. Send `WINCH` to the old master to gracefully stop its workers.\n\nYou can still roll back while the old master keeps its listen sockets:\n\n1. Send `HUP` to the old master to restart its workers without reloading the\n   config file.\n2. Send `TERM` to the new master to shut down its workers gracefully.\n3. Send `QUIT` to the new master to force it to exit.\n\nIf the new workers linger, send `KILL` after the new master quits.\n\nTo complete the upgrade, send `TERM` to the old master so only the new server\ncontinues running:\n\n```text\nPID USER      PR  NI  VIRT  RES  SHR S  %CPU %MEM    TIME+  COMMAND\n20854 benoitc   20   0 55748  12m 3348 S   0.0  0.2   0:00.45 gunicorn: master [test:app]\n20859 benoitc   20   0 55748  11m 1500 S   0.0  0.1   0:00.02 gunicorn: worker [test:app]\n20860 benoitc   20   0 55748  11m 1500 S   0.0  0.1   0:00.02 gunicorn: worker [test:app]\n20861 benoitc   20   0 55748  11m 1500 S   0.0  0.1   0:00.01 gunicorn: worker [test:app]\n```\n"
  },
  {
    "path": "docs/content/sponsor.md",
    "content": "# Support Gunicorn\n\nGunicorn has been serving Python web applications since 2010. It's downloaded millions of times per month and runs in production at companies of all sizes.\n\n**This project is maintained entirely by volunteers.** Your support helps ensure continued development, security updates, and compatibility with new Python versions.\n\n## Why Sponsor?\n\n- **Security**: Rapid response to vulnerabilities\n- **Reliability**: Bug fixes and stability improvements\n- **Compatibility**: Support for new Python versions and frameworks\n- **Features**: Continued development of ASGI, HTTP/2, and more\n- **Documentation**: Keeping guides and references up to date\n\n## How to Support\n\n### Donate\n\n<p>\n  <a href=\"https://github.com/sponsors/benoitc\"><img src=\"https://img.shields.io/badge/GitHub_Sponsors-❤-ea4aaa?style=for-the-badge&logo=github\" alt=\"GitHub Sponsors\"></a>\n  <a href=\"https://opencollective.com/gunicorn\"><img src=\"https://img.shields.io/badge/Open_Collective-Support-7FADF2?style=for-the-badge&logo=opencollective\" alt=\"Open Collective\"></a>\n  <a href=\"https://checkout.revolut.com/pay/c934e028-3a71-44eb-b99c-491342df2044\"><img src=\"https://img.shields.io/badge/Revolut-Donate-191c20?style=for-the-badge\" alt=\"Revolut\"></a>\n</p>\n\n- **[GitHub Sponsors](https://github.com/sponsors/benoitc)** - Monthly or one-time donations\n- **[Open Collective](https://opencollective.com/gunicorn)** - Transparent finances, tax-deductible in some regions\n- **[Revolut](https://checkout.revolut.com/pay/c934e028-3a71-44eb-b99c-491342df2044)** - Direct donations (individuals and companies)\n\n### Corporate Sponsorship\n\nIf gunicorn is part of your infrastructure, consider:\n\n- **Recurring sponsorship** through [GitHub Sponsors](https://github.com/sponsors/benoitc) or [Open Collective](https://opencollective.com/gunicorn)\n- **Sponsored support contracts** for priority bug fixes and feature requests\n- **Logo placement** on our website and README for sponsors\n\nFor corporate inquiries: [benoitc@enki-multimedia.eu](mailto:benoitc@enki-multimedia.eu)\n\n## Sponsors\n\nThank you to all our sponsors and contributors who make gunicorn possible!\n\n<!-- Sponsor logos will be added here -->\n\n---\n\n*Every contribution, no matter the size, helps keep gunicorn running. Thank you!*\n"
  },
  {
    "path": "docs/content/styles/overrides.css",
    "content": "/* Gunicorn Punchy Theme */\n:root {\n  --gunicorn-green: #00a650;\n  --gunicorn-green-dark: #008542;\n  --gunicorn-green-light: #00c853;\n  --gunicorn-teal: #00bfa5;\n  --gunicorn-bg: #fafafa;\n  --gunicorn-card: #ffffff;\n\n  --md-primary-fg-color: var(--gunicorn-green);\n  --md-primary-fg-color--light: var(--gunicorn-green-light);\n  --md-primary-fg-color--dark: var(--gunicorn-green-dark);\n  --md-accent-fg-color: var(--gunicorn-teal);\n  --md-typeset-a-color: var(--gunicorn-green);\n}\n\n[data-md-color-scheme=\"slate\"] {\n  --gunicorn-bg: #0d1117;\n  --gunicorn-card: #161b22;\n  --md-default-bg-color: #0d1117;\n  --md-default-bg-color--light: #161b22;\n}\n\n/* Header - punchy gradient */\n.md-header {\n  background: linear-gradient(135deg, var(--gunicorn-green-dark) 0%, var(--gunicorn-green) 100%);\n  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\n}\n\n.md-tabs {\n  background: linear-gradient(135deg, var(--gunicorn-green) 0%, var(--gunicorn-green-light) 100%);\n}\n\n/* Logo bigger */\n.md-header__button.md-logo img,\n.md-header__button.md-logo svg {\n  height: 2rem;\n}\n\n/* Version badge in header */\n.md-header__version {\n  margin-left: 0.5rem;\n  padding: 0.2rem 0.5rem;\n  font-size: 0.7rem;\n  font-weight: 600;\n  color: var(--gunicorn-green-dark);\n  background: rgba(255, 255, 255, 0.9);\n  border-radius: 4px;\n  text-decoration: none;\n  vertical-align: middle;\n}\n\n.md-header__version:hover {\n  background: #ffffff;\n  color: var(--gunicorn-green);\n}\n\n/* Navigation styling */\n.md-nav__link:hover {\n  color: var(--gunicorn-green);\n}\n\n.md-nav__link--active {\n  color: var(--gunicorn-green);\n  font-weight: 600;\n}\n\n/* Code blocks - punchy */\n.md-typeset code {\n  background: rgba(0, 166, 80, 0.08);\n  color: var(--gunicorn-green-dark);\n  border-radius: 4px;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset code {\n  background: rgba(0, 200, 83, 0.12);\n  color: var(--gunicorn-green-light);\n}\n\n.md-typeset pre {\n  border-radius: 8px;\n  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset pre {\n  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);\n}\n\n/* Admonitions - punchy colors */\n.md-typeset .admonition,\n.md-typeset details {\n  border-radius: 8px;\n  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);\n}\n\n.md-typeset .admonition.note,\n.md-typeset details.note {\n  border-color: var(--gunicorn-teal);\n}\n\n.md-typeset .note > .admonition-title,\n.md-typeset .note > summary {\n  background-color: rgba(0, 191, 165, 0.1);\n}\n\n.md-typeset .admonition.tip,\n.md-typeset details.tip {\n  border-color: var(--gunicorn-green);\n}\n\n.md-typeset .tip > .admonition-title,\n.md-typeset .tip > summary {\n  background-color: rgba(0, 166, 80, 0.1);\n}\n\n/* Tables - cleaner */\n.md-typeset table:not([class]) {\n  border-radius: 8px;\n  overflow: hidden;\n  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);\n}\n\n.md-typeset table:not([class]) th {\n  background: var(--gunicorn-green);\n  color: white;\n  font-weight: 600;\n}\n\n/* Buttons - punchy */\n.md-typeset .md-button {\n  border-radius: 8px;\n  font-weight: 600;\n  text-transform: none;\n  letter-spacing: 0;\n  transition: all 0.2s ease;\n}\n\n.md-typeset .md-button--primary {\n  background: linear-gradient(135deg, var(--gunicorn-green) 0%, var(--gunicorn-green-light) 100%);\n  border: none;\n  box-shadow: 0 4px 12px rgba(0, 166, 80, 0.3);\n}\n\n.md-typeset .md-button--primary:hover {\n  box-shadow: 0 6px 20px rgba(0, 166, 80, 0.4);\n  transform: translateY(-2px);\n}\n\n/* Search */\n.md-search__form {\n  border-radius: 8px;\n}\n\n/* Footer */\n.md-footer {\n  background: linear-gradient(135deg, var(--gunicorn-green-dark) 0%, #1a1a2e 100%);\n}\n\n.md-footer-meta {\n  background: rgba(0, 0, 0, 0.2);\n}\n\n/* Scrollbar */\n::-webkit-scrollbar {\n  width: 8px;\n  height: 8px;\n}\n\n::-webkit-scrollbar-thumb {\n  background: var(--gunicorn-green);\n  border-radius: 4px;\n}\n\n::-webkit-scrollbar-thumb:hover {\n  background: var(--gunicorn-green-light);\n}\n\n/* Selection */\n::selection {\n  background: rgba(0, 166, 80, 0.3);\n}\n\n/* ================================\n   Homepage Specific Styles\n   ================================ */\n\n/* These are for the non-custom template pages */\n.md-typeset .hero {\n  margin: 2rem 0 3rem;\n  padding: 3.5rem;\n  background: linear-gradient(135deg, var(--gunicorn-green-dark) 0%, var(--gunicorn-green) 50%, var(--gunicorn-teal) 100%);\n  color: #fff;\n  border-radius: 16px;\n  box-shadow: 0 20px 60px rgba(0, 166, 80, 0.25);\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .hero {\n  background: linear-gradient(135deg, #0d1117 0%, var(--gunicorn-green-dark) 50%, var(--gunicorn-green) 100%);\n  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);\n}\n\n.md-typeset .hero__inner {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 2.5rem;\n  align-items: center;\n  justify-content: space-between;\n}\n\n.md-typeset .hero__copy {\n  flex: 1 1 320px;\n  max-width: 520px;\n  font-size: 1.05rem;\n  line-height: 1.6;\n}\n\n.md-typeset .hero__copy h1 {\n  margin: 0 0 1rem;\n  font-size: 2.6rem;\n  font-weight: 800;\n  line-height: 1.15;\n  letter-spacing: -0.02em;\n}\n\n.md-typeset .hero__tagline {\n  font-size: 1.15rem;\n  opacity: 0.95;\n  margin-bottom: 0;\n}\n\n.md-typeset .hero__cta {\n  margin-top: 2rem;\n  display: flex;\n  flex-wrap: wrap;\n  gap: 1rem;\n}\n\n.md-typeset .hero__code {\n  flex: 1 1 260px;\n  max-width: 400px;\n  background: rgba(0, 0, 0, 0.25);\n  border-radius: 12px;\n  padding: 1.5rem;\n  backdrop-filter: blur(8px);\n  border: 1px solid rgba(255, 255, 255, 0.1);\n}\n\n.md-typeset .hero__code pre {\n  margin: 0 0 1rem;\n  border: none;\n  background: rgba(0, 0, 0, 0.4);\n  color: #e8f5ea;\n  box-shadow: none;\n}\n\n.md-typeset .hero__logo {\n  height: 72px;\n  margin-bottom: 1.5rem;\n  filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.2));\n}\n\n/* Pillars */\n.md-typeset .pillars {\n  display: grid;\n  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));\n  gap: 2rem;\n  margin: 3rem 0;\n}\n\n.md-typeset .pillar {\n  text-align: center;\n  padding: 2rem;\n  background: var(--gunicorn-card);\n  border-radius: 12px;\n  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);\n  transition: transform 0.2s ease, box-shadow 0.2s ease;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .pillar {\n  background: var(--gunicorn-card);\n  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);\n}\n\n.md-typeset .pillar:hover {\n  transform: translateY(-4px);\n  box-shadow: 0 12px 32px rgba(0, 166, 80, 0.15);\n}\n\n.md-typeset .pillar__icon {\n  font-size: 3rem;\n  margin-bottom: 1rem;\n}\n\n.md-typeset .pillar h3 {\n  margin: 0 0 0.5rem;\n  font-size: 1.3rem;\n  font-weight: 700;\n  color: var(--gunicorn-green-dark);\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .pillar h3 {\n  color: var(--gunicorn-green-light);\n}\n\n.md-typeset .pillar p {\n  margin: 0;\n  font-size: 0.95rem;\n  opacity: 0.8;\n}\n\n/* Frameworks */\n.md-typeset .frameworks {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 1rem;\n  justify-content: center;\n  margin: 2rem 0 3rem;\n}\n\n.md-typeset .framework {\n  background: var(--gunicorn-card);\n  border: 2px solid transparent;\n  border-radius: 50px;\n  padding: 0.75rem 1.75rem;\n  font-weight: 600;\n  font-size: 0.95rem;\n  color: var(--gunicorn-green-dark);\n  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);\n  transition: all 0.2s ease;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .framework {\n  background: var(--gunicorn-card);\n  color: #e8f5ea;\n}\n\n.md-typeset .framework:hover {\n  border-color: var(--gunicorn-green);\n  transform: translateY(-2px);\n  box-shadow: 0 8px 24px rgba(0, 166, 80, 0.2);\n}\n\n/* Feature Grid */\n.md-typeset .feature-grid {\n  display: grid;\n  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));\n  gap: 1.5rem;\n  margin: 2.5rem 0 3rem;\n}\n\n.md-typeset .feature-card {\n  background: var(--gunicorn-card);\n  border-radius: 12px;\n  padding: 1.75rem;\n  border: 1px solid rgba(0, 166, 80, 0.1);\n  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);\n  transition: all 0.2s ease;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .feature-card {\n  background: var(--gunicorn-card);\n  border-color: rgba(0, 200, 83, 0.15);\n  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);\n}\n\n.md-typeset .feature-card:hover {\n  transform: translateY(-4px);\n  border-color: var(--gunicorn-green);\n  box-shadow: 0 12px 32px rgba(0, 166, 80, 0.15);\n}\n\n.md-typeset .feature-card h3 {\n  margin-top: 0;\n  font-size: 1.2rem;\n  font-weight: 700;\n  color: var(--gunicorn-green-dark);\n  display: flex;\n  align-items: center;\n  gap: 0.5rem;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .feature-card h3 {\n  color: var(--gunicorn-green-light);\n}\n\n.md-typeset .feature-card p {\n  font-size: 0.95rem;\n  opacity: 0.8;\n  margin-bottom: 1rem;\n}\n\n.md-typeset .feature-card a {\n  display: inline-flex;\n  align-items: center;\n  gap: 0.35rem;\n  font-weight: 600;\n  color: var(--gunicorn-green);\n}\n\n.md-typeset .feature-card a:hover {\n  color: var(--gunicorn-green-light);\n}\n\n/* Badge */\n.md-typeset .badge {\n  display: inline-block;\n  font-size: 0.65rem;\n  font-weight: 700;\n  text-transform: uppercase;\n  padding: 0.2rem 0.6rem;\n  border-radius: 50px;\n  vertical-align: middle;\n  letter-spacing: 0.05em;\n}\n\n.md-typeset .badge--new {\n  background: linear-gradient(135deg, var(--gunicorn-green) 0%, var(--gunicorn-teal) 100%);\n  color: #fff;\n}\n\n/* Quick Links */\n.md-typeset .quick-links {\n  display: grid;\n  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n  gap: 1rem;\n  margin: 2rem 0;\n}\n\n.md-typeset .quick-link {\n  display: block;\n  padding: 1.5rem;\n  background: var(--gunicorn-card);\n  border-radius: 12px;\n  border: 2px solid transparent;\n  text-decoration: none;\n  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);\n  transition: all 0.2s ease;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .quick-link {\n  background: var(--gunicorn-card);\n}\n\n.md-typeset .quick-link:hover {\n  border-color: var(--gunicorn-green);\n  transform: translateY(-2px);\n  box-shadow: 0 8px 24px rgba(0, 166, 80, 0.15);\n}\n\n.md-typeset .quick-link strong {\n  display: block;\n  font-size: 1.1rem;\n  font-weight: 700;\n  color: var(--gunicorn-green-dark);\n  margin-bottom: 0.25rem;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .quick-link strong {\n  color: var(--gunicorn-green-light);\n}\n\n.md-typeset .quick-link span {\n  font-size: 0.9rem;\n  opacity: 0.7;\n}\n\n/* Community Links */\n.md-typeset .community-links {\n  margin: 1.5rem 0;\n}\n\n.md-typeset .community-links ul {\n  list-style: none;\n  padding: 0;\n  margin: 0;\n}\n\n.md-typeset .community-links li {\n  margin-bottom: 0.75rem;\n}\n\n/* Footer */\n.md-footer-meta__inner {\n  flex-wrap: wrap;\n}\n\n/* Responsive */\n@media (max-width: 960px) {\n  .md-typeset .hero {\n    padding: 2.5rem;\n  }\n\n  .md-typeset .hero__copy h1 {\n    font-size: 2rem;\n  }\n}\n\n@media (max-width: 720px) {\n  .md-typeset .hero {\n    margin-top: 1.5rem;\n    padding: 2rem;\n  }\n\n  .md-typeset .hero__cta {\n    flex-direction: column;\n    align-items: stretch;\n  }\n\n  .md-typeset .hero__code {\n    width: 100%;\n  }\n\n  .md-typeset .pillars {\n    grid-template-columns: 1fr;\n  }\n}\n"
  },
  {
    "path": "docs/content/uwsgi.md",
    "content": "# uWSGI Protocol\n\nGunicorn supports the uWSGI binary protocol, allowing it to receive requests from\nnginx using the `uwsgi_pass` directive. This provides efficient communication\nbetween nginx and Gunicorn without HTTP overhead.\n\nBoth **WSGI** and **ASGI** workers support the uWSGI protocol.\n\n!!! note\n    This is the **uWSGI binary protocol**, not the uWSGI server. Gunicorn\n    implements the protocol to receive requests from nginx, similar to how\n    the uWSGI server would.\n\n## Quick Start\n\nEnable uWSGI protocol support:\n\n```bash\n# WSGI application\ngunicorn myapp:app --protocol uwsgi --bind 127.0.0.1:8000\n\n# ASGI application\ngunicorn myapp:app --worker-class asgi --protocol uwsgi --bind 127.0.0.1:8000\n```\n\nConfigure nginx to forward requests:\n\n```nginx\nupstream gunicorn {\n    server 127.0.0.1:8000;\n}\n\nserver {\n    listen 80;\n    server_name example.com;\n\n    location / {\n        uwsgi_pass gunicorn;\n        include uwsgi_params;\n    }\n}\n```\n\n## Why Use uWSGI Protocol?\n\nThe uWSGI binary protocol offers several advantages over HTTP proxying:\n\n- **Lower overhead** - Binary format is more compact than HTTP headers\n- **Better integration** - nginx's native uwsgi module is highly optimized\n- **Simpler configuration** - No need to reconstruct HTTP headers\n\n## Configuration\n\n### Protocol Setting\n\nSwitch from HTTP to uWSGI protocol:\n\n```bash\ngunicorn myapp:app --protocol uwsgi\n```\n\nOr in a configuration file:\n\n```python\n# gunicorn.conf.py\nprotocol = \"uwsgi\"\n```\n\n### Allowed IPs\n\nBy default, uWSGI protocol requests are only accepted from localhost\n(`127.0.0.1` and `::1`). This prevents unauthorized hosts from sending\nrequests directly to Gunicorn.\n\nTo allow additional IPs:\n\n```bash\ngunicorn myapp:app --protocol uwsgi --uwsgi-allow-from 10.0.0.1,10.0.0.2\n```\n\nTo allow all IPs (not recommended for production):\n\n```bash\ngunicorn myapp:app --protocol uwsgi --uwsgi-allow-from '*'\n```\n\n!!! warning\n    Only allow IPs from trusted sources. The uWSGI protocol does not provide\n    authentication, so anyone who can connect can send requests.\n\n!!! note\n    UNIX socket connections are always allowed regardless of this setting.\n\n### Using UNIX Sockets\n\nFor better performance and security, use UNIX sockets instead of TCP:\n\n```bash\ngunicorn myapp:app --protocol uwsgi --bind unix:/run/gunicorn.sock\n```\n\nNginx configuration:\n\n```nginx\nupstream gunicorn {\n    server unix:/run/gunicorn.sock;\n}\n\nserver {\n    listen 80;\n\n    location / {\n        uwsgi_pass gunicorn;\n        include uwsgi_params;\n    }\n}\n```\n\n## Nginx Configuration\n\n### Basic Setup\n\nCreate or verify the `uwsgi_params` file exists (usually at `/etc/nginx/uwsgi_params`):\n\n```nginx\nuwsgi_param  QUERY_STRING       $query_string;\nuwsgi_param  REQUEST_METHOD     $request_method;\nuwsgi_param  CONTENT_TYPE       $content_type;\nuwsgi_param  CONTENT_LENGTH     $content_length;\n\nuwsgi_param  REQUEST_URI        $request_uri;\nuwsgi_param  PATH_INFO          $document_uri;\nuwsgi_param  DOCUMENT_ROOT      $document_root;\nuwsgi_param  SERVER_PROTOCOL    $server_protocol;\nuwsgi_param  REQUEST_SCHEME     $scheme;\nuwsgi_param  HTTPS              $https if_not_empty;\n\nuwsgi_param  REMOTE_ADDR        $remote_addr;\nuwsgi_param  REMOTE_PORT        $remote_port;\nuwsgi_param  SERVER_PORT        $server_port;\nuwsgi_param  SERVER_NAME        $server_name;\n```\n\n### With SSL Termination\n\nWhen nginx handles SSL and forwards to Gunicorn:\n\n```nginx\nserver {\n    listen 443 ssl;\n    server_name example.com;\n\n    ssl_certificate /path/to/cert.pem;\n    ssl_certificate_key /path/to/key.pem;\n\n    location / {\n        uwsgi_pass gunicorn;\n        include uwsgi_params;\n        uwsgi_param HTTPS on;\n    }\n}\n```\n\n### Load Balancing\n\nDistribute requests across multiple Gunicorn instances:\n\n```nginx\nupstream gunicorn {\n    least_conn;\n    server 127.0.0.1:8000;\n    server 127.0.0.1:8001;\n    server 127.0.0.1:8002;\n}\n\nserver {\n    listen 80;\n\n    location / {\n        uwsgi_pass gunicorn;\n        include uwsgi_params;\n    }\n}\n```\n\n### Static Files\n\nServe static files directly from nginx:\n\n```nginx\nserver {\n    listen 80;\n\n    location /static/ {\n        alias /path/to/static/;\n    }\n\n    location / {\n        uwsgi_pass gunicorn;\n        include uwsgi_params;\n    }\n}\n```\n\n## Protocol Details\n\nThe uWSGI protocol uses a compact binary format:\n\n| Bytes | Field | Description |\n|-------|-------|-------------|\n| 0 | modifier1 | Packet type (0 = WSGI request) |\n| 1-2 | datasize | Size of vars block (little-endian) |\n| 3 | modifier2 | Additional flags (usually 0) |\n\nAfter the header, the vars block contains CGI-style key-value pairs:\n\n```\n[2-byte key_size][key][2-byte val_size][value]...\n```\n\nStandard CGI variables like `REQUEST_METHOD`, `PATH_INFO`, and `QUERY_STRING`\nare extracted from this block to construct the WSGI environ.\n\n## Combining with HTTP\n\nYou can run Gunicorn with both HTTP and uWSGI protocol support by running\nseparate instances:\n\n```bash\n# HTTP for direct access\ngunicorn myapp:app --bind 127.0.0.1:8080\n\n# uWSGI for nginx\ngunicorn myapp:app --protocol uwsgi --bind 127.0.0.1:8000\n```\n\n## Troubleshooting\n\n### ForbiddenUWSGIRequest Error\n\nIf you see \"Forbidden uWSGI request from IP\", the connecting IP is not in\nthe allowed list. Either:\n\n1. Add the IP to `--uwsgi-allow-from`\n2. Use UNIX sockets instead\n3. Ensure nginx is connecting from an allowed IP\n\n### Invalid uWSGI Header\n\nThis usually means:\n\n1. HTTP traffic is being sent to a uWSGI endpoint\n2. The packet is malformed or truncated\n3. Network issues caused data corruption\n\nVerify that nginx is using `uwsgi_pass` (not `proxy_pass`) and that the\n`uwsgi_params` file is being included.\n\n### Headers Missing\n\nIf certain headers aren't reaching your application, verify they're included\nin `uwsgi_params`. Custom headers should be passed as:\n\n```nginx\nuwsgi_param HTTP_X_CUSTOM_HEADER $http_x_custom_header;\n```\n\n## See Also\n\n- [Settings Reference](reference/settings.md#protocol) - Protocol and uWSGI settings\n- [Deploy](deploy.md) - General deployment guidance\n- [Design](design.md) - Worker architecture overview\n"
  },
  {
    "path": "docs/macros.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom importlib import import_module\n\ndef define_env(env):\n    \"\"\"Register template variables for MkDocs macros.\"\"\"\n    gunicorn = import_module(\"gunicorn\")\n    env.variables.update(\n        release=gunicorn.__version__,\n        version=gunicorn.__version__,\n        github_repo=\"https://github.com/benoitc/gunicorn\",\n        pypi_url=f\"https://pypi.org/project/gunicorn/{gunicorn.__version__}/\",\n    )\n"
  },
  {
    "path": "examples/alt_spec.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n#\n# An example of how to pass information from the command line to\n# a WSGI app. Only applies to the native WSGI workers used by\n# Gunicorn sync (default) workers.\n#\n#   $ gunicorn 'alt_spec:load(arg)'\n#\n# Single quoting is generally necessary for shell escape semantics.\n#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\ndef load(arg):\n    def app(environ, start_response):\n        data = b'Hello, %s!\\n' % arg\n        status = '200 OK'\n        response_headers = [\n            ('Content-type', 'text/plain'),\n            ('Content-Length', str(len(data)))\n        ]\n        start_response(status, response_headers)\n        return iter([data])\n    return app\n"
  },
  {
    "path": "examples/asgi/__init__.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nASGI example applications for gunicorn.\n\"\"\"\n"
  },
  {
    "path": "examples/asgi/basic_app.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nBasic ASGI application example.\n\nRun with:\n    gunicorn -k asgi examples.asgi.basic_app:app\n\nTest with:\n    curl http://127.0.0.1:8000/\n    curl http://127.0.0.1:8000/hello\n    curl -X POST http://127.0.0.1:8000/echo -d \"test data\"\n\"\"\"\n\n\nasync def app(scope, receive, send):\n    \"\"\"Simple ASGI application demonstrating basic functionality.\"\"\"\n\n    if scope[\"type\"] == \"lifespan\":\n        await handle_lifespan(scope, receive, send)\n    elif scope[\"type\"] == \"http\":\n        await handle_http(scope, receive, send)\n    else:\n        raise ValueError(f\"Unknown scope type: {scope['type']}\")\n\n\nasync def handle_lifespan(scope, receive, send):\n    \"\"\"Handle lifespan events (startup/shutdown).\"\"\"\n    while True:\n        message = await receive()\n        if message[\"type\"] == \"lifespan.startup\":\n            print(\"ASGI application starting up...\")\n            await send({\"type\": \"lifespan.startup.complete\"})\n        elif message[\"type\"] == \"lifespan.shutdown\":\n            print(\"ASGI application shutting down...\")\n            await send({\"type\": \"lifespan.shutdown.complete\"})\n            return\n\n\nasync def handle_http(scope, receive, send):\n    \"\"\"Handle HTTP requests.\"\"\"\n    path = scope[\"path\"]\n    method = scope[\"method\"]\n\n    if path == \"/\" and method == \"GET\":\n        await send_response(send, 200, b\"Welcome to gunicorn ASGI!\\n\")\n\n    elif path == \"/hello\" and method == \"GET\":\n        name = get_query_param(scope, \"name\", \"World\")\n        body = f\"Hello, {name}!\\n\".encode()\n        await send_response(send, 200, body)\n\n    elif path == \"/echo\" and method == \"POST\":\n        body = await read_body(receive)\n        await send_response(send, 200, body, content_type=b\"application/octet-stream\")\n\n    elif path == \"/headers\":\n        headers_info = format_headers(scope[\"headers\"])\n        await send_response(send, 200, headers_info.encode())\n\n    elif path == \"/info\":\n        info = format_request_info(scope)\n        await send_response(send, 200, info.encode(), content_type=b\"application/json\")\n\n    else:\n        await send_response(send, 404, b\"Not Found\\n\")\n\n\nasync def send_response(send, status, body, content_type=b\"text/plain\"):\n    \"\"\"Send an HTTP response.\"\"\"\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": status,\n        \"headers\": [\n            (b\"content-type\", content_type),\n            (b\"content-length\", str(len(body)).encode()),\n        ],\n    })\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": body,\n    })\n\n\nasync def read_body(receive):\n    \"\"\"Read the full request body.\"\"\"\n    body = b\"\"\n    while True:\n        message = await receive()\n        body += message.get(\"body\", b\"\")\n        if not message.get(\"more_body\", False):\n            break\n    return body\n\n\ndef get_query_param(scope, name, default=None):\n    \"\"\"Get a query parameter value.\"\"\"\n    query_string = scope.get(\"query_string\", b\"\").decode()\n    for param in query_string.split(\"&\"):\n        if \"=\" in param:\n            key, value = param.split(\"=\", 1)\n            if key == name:\n                return value\n    return default\n\n\ndef format_headers(headers):\n    \"\"\"Format headers for display.\"\"\"\n    lines = [\"Request Headers:\"]\n    for name, value in headers:\n        lines.append(f\"  {name.decode()}: {value.decode()}\")\n    return \"\\n\".join(lines) + \"\\n\"\n\n\ndef format_request_info(scope):\n    \"\"\"Format request info as JSON.\"\"\"\n    import json\n    info = {\n        \"method\": scope[\"method\"],\n        \"path\": scope[\"path\"],\n        \"query_string\": scope.get(\"query_string\", b\"\").decode(),\n        \"http_version\": scope[\"http_version\"],\n        \"scheme\": scope[\"scheme\"],\n        \"server\": list(scope.get(\"server\") or []),\n        \"client\": list(scope.get(\"client\") or []),\n        \"root_path\": scope.get(\"root_path\", \"\"),\n    }\n    return json.dumps(info, indent=2) + \"\\n\"\n"
  },
  {
    "path": "examples/asgi/websocket_app.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nWebSocket ASGI application example.\n\nRun with:\n    gunicorn -k asgi examples.asgi.websocket_app:app\n\nTest with:\n    # Using websocat (install with: cargo install websocat)\n    websocat ws://127.0.0.1:8000/ws\n\n    # Or using Python websockets library\n    python -c \"\n    import asyncio\n    import websockets\n    async def test():\n        async with websockets.connect('ws://127.0.0.1:8000/ws') as ws:\n            await ws.send('Hello')\n            print(await ws.recv())\n    asyncio.run(test())\n    \"\n\"\"\"\n\n\nasync def app(scope, receive, send):\n    \"\"\"ASGI application with WebSocket support.\"\"\"\n\n    if scope[\"type\"] == \"lifespan\":\n        await handle_lifespan(scope, receive, send)\n    elif scope[\"type\"] == \"http\":\n        await handle_http(scope, receive, send)\n    elif scope[\"type\"] == \"websocket\":\n        await handle_websocket(scope, receive, send)\n    else:\n        raise ValueError(f\"Unknown scope type: {scope['type']}\")\n\n\nasync def handle_lifespan(scope, receive, send):\n    \"\"\"Handle lifespan events.\"\"\"\n    while True:\n        message = await receive()\n        if message[\"type\"] == \"lifespan.startup\":\n            await send({\"type\": \"lifespan.startup.complete\"})\n        elif message[\"type\"] == \"lifespan.shutdown\":\n            await send({\"type\": \"lifespan.shutdown.complete\"})\n            return\n\n\nasync def handle_http(scope, receive, send):\n    \"\"\"Handle HTTP requests - serve a simple HTML page for WebSocket testing.\"\"\"\n    path = scope[\"path\"]\n\n    if path == \"/\":\n        html = HTML_PAGE.encode()\n        await send({\n            \"type\": \"http.response.start\",\n            \"status\": 200,\n            \"headers\": [\n                (b\"content-type\", b\"text/html\"),\n                (b\"content-length\", str(len(html)).encode()),\n            ],\n        })\n        await send({\n            \"type\": \"http.response.body\",\n            \"body\": html,\n        })\n    else:\n        await send({\n            \"type\": \"http.response.start\",\n            \"status\": 404,\n            \"headers\": [(b\"content-type\", b\"text/plain\")],\n        })\n        await send({\n            \"type\": \"http.response.body\",\n            \"body\": b\"Not Found\",\n        })\n\n\nasync def handle_websocket(scope, receive, send):\n    \"\"\"Handle WebSocket connections.\"\"\"\n    path = scope[\"path\"]\n\n    if path == \"/ws\":\n        await echo_websocket(scope, receive, send)\n    elif path == \"/ws/chat\":\n        await chat_websocket(scope, receive, send)\n    else:\n        # Reject the connection\n        await send({\"type\": \"websocket.close\", \"code\": 4004})\n\n\nasync def echo_websocket(scope, receive, send):\n    \"\"\"Echo WebSocket - sends back whatever it receives.\"\"\"\n    # Wait for connection\n    message = await receive()\n    if message[\"type\"] != \"websocket.connect\":\n        return\n\n    # Accept the connection\n    await send({\"type\": \"websocket.accept\"})\n\n    # Echo loop\n    try:\n        while True:\n            message = await receive()\n\n            if message[\"type\"] == \"websocket.disconnect\":\n                break\n\n            if message[\"type\"] == \"websocket.receive\":\n                if \"text\" in message:\n                    # Echo text back\n                    await send({\n                        \"type\": \"websocket.send\",\n                        \"text\": f\"Echo: {message['text']}\"\n                    })\n                elif \"bytes\" in message:\n                    # Echo bytes back\n                    await send({\n                        \"type\": \"websocket.send\",\n                        \"bytes\": message[\"bytes\"]\n                    })\n    except Exception as e:\n        print(f\"WebSocket error: {e}\")\n    finally:\n        try:\n            await send({\"type\": \"websocket.close\", \"code\": 1000})\n        except Exception:\n            pass\n\n\nasync def chat_websocket(scope, receive, send):\n    \"\"\"Chat WebSocket - simple broadcast example.\"\"\"\n    message = await receive()\n    if message[\"type\"] != \"websocket.connect\":\n        return\n\n    await send({\n        \"type\": \"websocket.accept\",\n        \"subprotocol\": \"chat\"\n    })\n\n    await send({\n        \"type\": \"websocket.send\",\n        \"text\": \"Welcome to the chat! Send messages and they will be echoed back.\"\n    })\n\n    try:\n        while True:\n            message = await receive()\n\n            if message[\"type\"] == \"websocket.disconnect\":\n                break\n\n            if message[\"type\"] == \"websocket.receive\" and \"text\" in message:\n                text = message[\"text\"]\n                await send({\n                    \"type\": \"websocket.send\",\n                    \"text\": f\"[You]: {text}\"\n                })\n    except Exception:\n        pass\n\n\nHTML_PAGE = \"\"\"<!DOCTYPE html>\n<html>\n<head>\n    <title>WebSocket Test</title>\n    <style>\n        body { font-family: sans-serif; max-width: 800px; margin: 50px auto; padding: 20px; }\n        #messages { border: 1px solid #ccc; height: 300px; overflow-y: auto; padding: 10px; margin-bottom: 10px; }\n        #input { width: 80%; padding: 10px; }\n        button { padding: 10px 20px; }\n        .sent { color: blue; }\n        .received { color: green; }\n        .error { color: red; }\n    </style>\n</head>\n<body>\n    <h1>WebSocket Test</h1>\n    <div id=\"messages\"></div>\n    <input type=\"text\" id=\"input\" placeholder=\"Type a message...\">\n    <button onclick=\"sendMessage()\">Send</button>\n    <button onclick=\"connectWS()\">Connect</button>\n    <button onclick=\"disconnectWS()\">Disconnect</button>\n\n    <script>\n        let ws = null;\n        const messages = document.getElementById('messages');\n        const input = document.getElementById('input');\n\n        function log(msg, className) {\n            const div = document.createElement('div');\n            div.className = className || '';\n            div.textContent = msg;\n            messages.appendChild(div);\n            messages.scrollTop = messages.scrollHeight;\n        }\n\n        function connectWS() {\n            if (ws) {\n                log('Already connected', 'error');\n                return;\n            }\n            ws = new WebSocket('ws://' + window.location.host + '/ws');\n            ws.onopen = () => log('Connected!', 'received');\n            ws.onclose = () => { log('Disconnected', 'error'); ws = null; };\n            ws.onerror = (e) => log('Error: ' + e, 'error');\n            ws.onmessage = (e) => log(e.data, 'received');\n        }\n\n        function disconnectWS() {\n            if (ws) ws.close();\n        }\n\n        function sendMessage() {\n            if (!ws) { log('Not connected', 'error'); return; }\n            const msg = input.value;\n            if (!msg) return;\n            ws.send(msg);\n            log('Sent: ' + msg, 'sent');\n            input.value = '';\n        }\n\n        input.onkeypress = (e) => { if (e.key === 'Enter') sendMessage(); };\n\n        // Auto-connect\n        connectWS();\n    </script>\n</body>\n</html>\n\"\"\"\n"
  },
  {
    "path": "examples/bad.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport tempfile\nfiles = []\ndef app(environ, start_response):\n    files.append(tempfile.mkstemp())\n    start_response('200 OK', [('Content-type', 'text/plain'), ('Content-length', '2')])\n    return ['ok']\n"
  },
  {
    "path": "examples/boot_fail.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nraise RuntimeError(\"Bad app!\")\n\ndef app(environ, start_response):\n    assert 1 == 2, \"Shouldn't get here.\"\n"
  },
  {
    "path": "examples/celery_alternative/Dockerfile",
    "content": "# Dockerfile for Celery Replacement Example\n#\n# This demonstrates running a production-ready application with\n# Gunicorn dirty arbiters replacing Celery for background tasks.\n#\n# Key difference from Celery deployment:\n# - Celery: Needs separate web + worker containers + Redis/RabbitMQ\n# - Dirty: Single container handles both HTTP and background tasks\n\nFROM python:3.12-slim\n\n# Set working directory\nWORKDIR /app\n\n# Install system dependencies\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n    curl \\\n    && rm -rf /var/lib/apt/lists/*\n\n# Copy gunicorn source and install (from build context root)\nCOPY . /gunicorn-src\nRUN pip install --no-cache-dir /gunicorn-src\n\n# Copy example application\nCOPY examples/celery_alternative /app\nRUN pip install --no-cache-dir fastapi uvloop requests pytest\n\n# Environment variables\nENV PYTHONUNBUFFERED=1\nENV PYTHONDONTWRITEBYTECODE=1\nENV PYTHONPATH=/gunicorn-src\nENV GUNICORN_BIND=0.0.0.0:8000\nENV GUNICORN_WORKERS=4\nENV DIRTY_WORKERS=9\nENV DIRTY_TIMEOUT=300\nENV LOG_LEVEL=info\n\n# Expose port\nEXPOSE 8000\n\n# Health check\nHEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \\\n    CMD curl -f http://localhost:8000/health || exit 1\n\n# Run gunicorn with dirty arbiters\nCMD [\"gunicorn\", \"-c\", \"gunicorn_conf.py\", \"app:app\"]\n"
  },
  {
    "path": "examples/celery_alternative/README.md",
    "content": "# Celery Alternative Example\n\nThis example demonstrates how to replace Celery with Gunicorn's **dirty arbiters** for background task processing, using **async ASGI** for non-blocking HTTP handling.\n\n## Why Use This Instead of Celery?\n\n### The Problem with Celery\n\nCelery requires:\n- An external message broker (Redis or RabbitMQ)\n- Separate worker processes (`celery -A app worker`)\n- Stateless workers that reload models/connections on every task\n- Polling or WebSockets for progress updates\n\n### What Dirty Arbiters Provide\n\n| Feature | Celery | Dirty Arbiters |\n|---------|--------|----------------|\n| **External broker** | Required (Redis/RabbitMQ) | None - uses Unix sockets |\n| **Deployment** | Multiple processes | Single `gunicorn` command |\n| **Worker state** | Stateless | Stateful - keep ML models, DB connections loaded |\n| **Progress updates** | Polling or WebSocket | Native streaming |\n| **HTTP blocking** | N/A (separate process) | Non-blocking with async ASGI |\n\n### When to Use Dirty Arbiters\n\n**Good fit:**\n- Tasks that benefit from keeping state (ML models, DB connection pools, caches)\n- Tasks where you want immediate results (not fire-and-forget)\n- Real-time progress streaming\n- Simpler deployment without external dependencies\n\n**Not ideal for:**\n- True fire-and-forget queuing with persistence\n- Distributed task execution across multiple machines\n- Tasks that must survive server restarts\n\n## How It Works\n\n```\n┌─────────────────────────────────────────────────────────────┐\n│                    Gunicorn Master                          │\n├─────────────────────────────────────────────────────────────┤\n│                                                             │\n│  ┌─────────────────────────────────────────────────────┐   │\n│  │              ASGI Workers (uvloop)                   │   │\n│  │   Non-blocking! One worker handles many requests     │   │\n│  │   await client.execute_async() doesn't block         │   │\n│  └──────────────────────────┬──────────────────────────┘   │\n│                             │                               │\n│                       Unix Socket IPC                       │\n│                             │                               │\n│  ┌──────────────────────────┼──────────────────────────┐   │\n│  │                Dirty Workers (Stateful)              │   │\n│  │                                                      │   │\n│  │  ┌────────────┐  ┌────────────┐  ┌────────────┐     │   │\n│  │  │EmailWorker │  │ImageWorker │  │DataWorker  │ ... │   │\n│  │  │ (2 procs)  │  │ (2 procs)  │  │ (4 procs)  │     │   │\n│  │  │            │  │            │  │            │     │   │\n│  │  │ SMTP conn  │  │ PIL loaded │  │ DB pool    │     │   │\n│  │  │ kept alive │  │ in memory  │  │ cached     │     │   │\n│  │  └────────────┘  └────────────┘  └────────────┘     │   │\n│  │                                                      │   │\n│  │                    Dirty Arbiter                     │   │\n│  └──────────────────────────────────────────────────────┘   │\n│                                                             │\n└─────────────────────────────────────────────────────────────┘\n```\n\n**Key insight:** The HTTP workers use async I/O, so `await client.execute_async()` doesn't block the event loop. One ASGI worker can handle thousands of concurrent requests while waiting for dirty workers to complete tasks.\n\n## Quick Start\n\n### Local Development\n\n```bash\n# Install dependencies\npip install fastapi uvloop httpx pytest pytest-asyncio\npip install -e ../..  # Install gunicorn from source\n\n# Run the application\ngunicorn -c gunicorn_conf.py app:app\n\n# In another terminal, test it\ncurl http://localhost:8000/health\ncurl -X POST http://localhost:8000/api/email/send \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"to\": \"test@example.com\", \"subject\": \"Hello\", \"body\": \"World\"}'\n\n# Interactive API docs\nopen http://localhost:8000/docs\n```\n\n### Docker\n\n```bash\n# Build and run\ndocker compose up --build\n\n# Run with tests\ndocker compose --profile test up --build --abort-on-container-exit\n```\n\n## Task Workers\n\nEach worker class maintains state across requests:\n\n### EmailWorker (2 workers)\n- Keeps SMTP connection alive\n- `send_email(to, subject, body)` - Send single email\n- `send_bulk_emails(recipients, subject, body)` - Bulk send with streaming progress\n\n### ImageWorker (2 workers)\n- Keeps PIL/image libraries loaded\n- `resize(image_data, width, height)` - Resize image\n- `process_batch(images, operation)` - Batch process with streaming\n\n### DataWorker (4 workers)\n- Maintains DB connection pool and query cache\n- `aggregate(data, group_by, agg_field)` - Aggregate data\n- `etl_pipeline(source_data, transformations)` - ETL with streaming progress\n- `cached_query(query_key, ttl)` - Query with in-memory caching\n\n### ScheduledWorker (1 worker)\n- For periodic tasks (call from cron)\n- `cleanup_old_files(directory, max_age_days)`\n- `generate_daily_report()`\n\n## Streaming Progress Example\n\nReal-time progress without polling:\n\n```python\nimport httpx\nimport json\n\nasync with httpx.AsyncClient() as client:\n    async with client.stream(\n        \"POST\",\n        \"http://localhost:8000/api/email/send-bulk\",\n        json={\n            \"recipients\": [\"a@x.com\", \"b@x.com\", \"c@x.com\"],\n            \"subject\": \"Newsletter\",\n            \"body\": \"Hello!\",\n        },\n    ) as response:\n        async for line in response.aiter_lines():\n            if line.startswith(\"data: \"):\n                progress = json.loads(line[6:])\n                if progress[\"type\"] == \"progress\":\n                    print(f\"Progress: {progress['percent']}%\")\n                elif progress[\"type\"] == \"complete\":\n                    print(f\"Done! Sent: {progress['sent']}\")\n```\n\n## Celery Migration Guide\n\n### Before (Celery)\n\n```python\n# tasks.py\nfrom celery import Celery\n\napp = Celery('tasks', broker='redis://localhost')\n\n@app.task\ndef send_email(to, subject, body):\n    smtp = smtplib.SMTP(...)  # New connection every task!\n    smtp.send(...)\n    return {\"status\": \"sent\"}\n\n@app.task(bind=True)\ndef send_bulk(self, recipients, subject, body):\n    for i, to in enumerate(recipients):\n        send_email(to, subject, body)\n        self.update_state(state='PROGRESS', meta={'current': i})  # Requires polling!\n```\n\n```python\n# views.py - Flask\nfrom tasks import send_email\n\n@app.route('/send')\ndef send_view():\n    send_email.delay(to, subject, body)  # Fire and forget\n    return {\"status\": \"queued\"}  # Can't get result without polling\n```\n\n### After (Dirty Arbiters)\n\n```python\n# tasks.py\nfrom gunicorn.dirty.app import DirtyApp\n\nclass EmailWorker(DirtyApp):\n    workers = 2\n\n    def init(self):\n        self.smtp = smtplib.SMTP(...)  # Connected once, reused!\n\n    def __call__(self, action, *args, **kwargs):\n        return getattr(self, action)(*args, **kwargs)\n\n    def send_email(self, to, subject, body):\n        self.smtp.send(...)  # Reuses connection\n        return {\"status\": \"sent\"}\n\n    def send_bulk(self, recipients, subject, body):\n        for i, to in enumerate(recipients):\n            self.send_email(to, subject, body)\n            yield {\"type\": \"progress\", \"current\": i}  # Native streaming!\n```\n\n```python\n# views.py - FastAPI (async)\nfrom gunicorn.dirty import get_dirty_client_async\n\n@app.post('/send')\nasync def send_view(data: EmailRequest):\n    client = await get_dirty_client_async()\n    # Non-blocking! Other requests handled while waiting\n    result = await client.execute_async(\"tasks:EmailWorker\", \"send_email\", ...)\n    return result  # Immediate result, no polling!\n```\n\n## Configuration\n\n```python\n# gunicorn_conf.py\n\n# ASGI workers for non-blocking HTTP\nworker_class = \"asgi\"\nasgi_loop = \"uvloop\"\nworkers = 4\n\n# Dirty workers (replace Celery)\ndirty_apps = [\n    \"tasks:EmailWorker\",\n    \"tasks:ImageWorker\",\n    \"tasks:DataWorker\",\n]\ndirty_workers = 9\ndirty_timeout = 300\n```\n\n## Running Tests\n\n```bash\n# Unit tests (no server needed)\npytest tests/test_tasks.py -v\n\n# Integration tests (server must be running)\nAPP_URL=http://localhost:8000 pytest tests/test_integration.py -v\n\n# All tests via Docker\ndocker compose --profile test up --build --abort-on-container-exit\n```\n\n## API Endpoints\n\nVisit `/docs` for interactive Swagger documentation.\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| `/api/email/send` | POST | Send single email |\n| `/api/email/send-bulk` | POST | Bulk send (SSE streaming) |\n| `/api/image/resize` | POST | Resize image |\n| `/api/image/process-batch` | POST | Batch process (SSE streaming) |\n| `/api/data/aggregate` | POST | Aggregate data |\n| `/api/data/etl` | POST | ETL pipeline (SSE streaming) |\n| `/api/data/query` | POST | Cached query |\n| `/api/scheduled/*` | POST | Scheduled tasks |\n| `/health` | GET | Health check |\n"
  },
  {
    "path": "examples/celery_alternative/app.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nWeb Application - FastAPI app demonstrating Celery replacement.\n\nThis shows how to call dirty arbiter tasks from your web application\nusing the async API, which doesn't block the event loop.\n\nKey difference from sync (Flask/gthread):\n- `await client.execute_async()` is non-blocking\n- A single worker can handle many concurrent requests\n- True async I/O - other requests proceed while waiting for task results\n\"\"\"\n\nimport json\nfrom contextlib import asynccontextmanager\n\nfrom fastapi import FastAPI, HTTPException\nfrom fastapi.responses import StreamingResponse\nfrom pydantic import BaseModel\n\nfrom gunicorn.dirty import get_dirty_client_async\nfrom gunicorn.dirty.errors import (\n    DirtyError,\n    DirtyTimeoutError,\n)\n\n\n# Task worker import paths (like Celery task names)\nEMAIL_WORKER = \"examples.celery_alternative.tasks:EmailWorker\"\nIMAGE_WORKER = \"examples.celery_alternative.tasks:ImageWorker\"\nDATA_WORKER = \"examples.celery_alternative.tasks:DataWorker\"\nSCHEDULED_WORKER = \"examples.celery_alternative.tasks:ScheduledWorker\"\n\n\n@asynccontextmanager\nasync def lifespan(app: FastAPI):\n    \"\"\"Application lifespan - startup and shutdown.\"\"\"\n    yield\n\n\napp = FastAPI(\n    title=\"Celery Replacement Demo\",\n    description=\"Demonstrating Gunicorn dirty arbiters as Celery replacement with async ASGI\",\n    lifespan=lifespan,\n)\n\n\n# ============================================================================\n# Request/Response Models\n# ============================================================================\n\nclass EmailRequest(BaseModel):\n    to: str\n    subject: str\n    body: str\n    html: bool = False\n\n\nclass BulkEmailRequest(BaseModel):\n    recipients: list[str]\n    subject: str\n    body: str\n\n\nclass ImageResizeRequest(BaseModel):\n    image_data: str = \"\"\n    width: int = 800\n    height: int = 600\n\n\nclass ThumbnailRequest(BaseModel):\n    image_data: str = \"\"\n    size: int = 150\n\n\nclass ImageBatchRequest(BaseModel):\n    images: list[dict]\n    operation: str = \"resize\"\n    width: int = 800\n    height: int = 600\n    size: int = 150\n\n\nclass AggregateRequest(BaseModel):\n    data: list[dict]\n    group_by: str\n    agg_field: str\n    agg_func: str = \"sum\"\n\n\nclass ETLRequest(BaseModel):\n    source_data: list[dict]\n    transformations: list[dict] = []\n\n\nclass QueryRequest(BaseModel):\n    query_key: str\n    ttl: int = 300\n\n\nclass CleanupRequest(BaseModel):\n    directory: str = \"/tmp\"\n    max_age_days: int = 7\n\n\nclass SyncRequest(BaseModel):\n    source: str = \"default\"\n\n\n# ============================================================================\n# Email Tasks - Like Celery email tasks\n# ============================================================================\n\n@app.post(\"/api/email/send\")\nasync def send_email(data: EmailRequest):\n    \"\"\"\n    Send a single email.\n\n    Celery equivalent:\n        send_email.delay(to, subject, body)\n\n    With async dirty client, this doesn't block the event loop!\n    Other requests can be handled while waiting for the task.\n    \"\"\"\n    try:\n        client = await get_dirty_client_async()\n        result = await client.execute_async(\n            EMAIL_WORKER,\n            \"send_email\",\n            to=data.to,\n            subject=data.subject,\n            body=data.body,\n            html=data.html,\n        )\n        return result\n    except DirtyTimeoutError:\n        raise HTTPException(status_code=504, detail=\"Task timed out\")\n    except DirtyError as e:\n        raise HTTPException(status_code=500, detail=str(e))\n\n\n@app.post(\"/api/email/send-bulk\")\nasync def send_bulk_emails(data: BulkEmailRequest):\n    \"\"\"\n    Send bulk emails with streaming progress.\n\n    Celery equivalent:\n        result = send_bulk.apply_async([recipients, subject, body])\n        while not result.ready():\n            print(result.info)  # Progress polling\n\n    With dirty arbiters, progress is streamed in real-time!\n    \"\"\"\n    async def generate():\n        try:\n            client = await get_dirty_client_async()\n            async for progress in client.stream_async(\n                EMAIL_WORKER,\n                \"send_bulk_emails\",\n                recipients=data.recipients,\n                subject=data.subject,\n                body=data.body,\n            ):\n                yield f\"data: {json.dumps(progress)}\\n\\n\"\n        except DirtyError as e:\n            yield f\"data: {json.dumps({'error': str(e)})}\\n\\n\"\n\n    return StreamingResponse(\n        generate(),\n        media_type=\"text/event-stream\",\n        headers={\n            \"Cache-Control\": \"no-cache\",\n            \"X-Accel-Buffering\": \"no\",\n        },\n    )\n\n\n@app.get(\"/api/email/stats\")\nasync def email_stats():\n    \"\"\"Get email worker statistics.\"\"\"\n    try:\n        client = await get_dirty_client_async()\n        result = await client.execute_async(EMAIL_WORKER, \"stats\")\n        return result\n    except DirtyError as e:\n        raise HTTPException(status_code=500, detail=str(e))\n\n\n# ============================================================================\n# Image Tasks - Like Celery image processing tasks\n# ============================================================================\n\n@app.post(\"/api/image/resize\")\nasync def resize_image(data: ImageResizeRequest):\n    \"\"\"\n    Resize an image.\n\n    Celery equivalent:\n        resize_image.delay(image_data, width, height)\n    \"\"\"\n    try:\n        client = await get_dirty_client_async()\n        result = await client.execute_async(\n            IMAGE_WORKER,\n            \"resize\",\n            image_data=data.image_data,\n            width=data.width,\n            height=data.height,\n        )\n        return result\n    except DirtyError as e:\n        raise HTTPException(status_code=500, detail=str(e))\n\n\n@app.post(\"/api/image/thumbnail\")\nasync def generate_thumbnail(data: ThumbnailRequest):\n    \"\"\"Generate a thumbnail.\"\"\"\n    try:\n        client = await get_dirty_client_async()\n        result = await client.execute_async(\n            IMAGE_WORKER,\n            \"generate_thumbnail\",\n            image_data=data.image_data,\n            size=data.size,\n        )\n        return result\n    except DirtyError as e:\n        raise HTTPException(status_code=500, detail=str(e))\n\n\n@app.post(\"/api/image/process-batch\")\nasync def process_image_batch(data: ImageBatchRequest):\n    \"\"\"\n    Process multiple images with progress streaming.\n    \"\"\"\n    async def generate():\n        try:\n            client = await get_dirty_client_async()\n            async for progress in client.stream_async(\n                IMAGE_WORKER,\n                \"process_batch\",\n                images=data.images,\n                operation=data.operation,\n                width=data.width,\n                height=data.height,\n                size=data.size,\n            ):\n                yield f\"data: {json.dumps(progress)}\\n\\n\"\n        except DirtyError as e:\n            yield f\"data: {json.dumps({'error': str(e)})}\\n\\n\"\n\n    return StreamingResponse(\n        generate(),\n        media_type=\"text/event-stream\",\n    )\n\n\n@app.get(\"/api/image/stats\")\nasync def image_stats():\n    \"\"\"Get image worker statistics.\"\"\"\n    try:\n        client = await get_dirty_client_async()\n        result = await client.execute_async(IMAGE_WORKER, \"stats\")\n        return result\n    except DirtyError as e:\n        raise HTTPException(status_code=500, detail=str(e))\n\n\n# ============================================================================\n# Data Tasks - Like Celery data processing tasks\n# ============================================================================\n\n@app.post(\"/api/data/aggregate\")\nasync def aggregate_data(data: AggregateRequest):\n    \"\"\"\n    Aggregate data.\n\n    Celery equivalent:\n        aggregate_data.delay(data, group_by, agg_field, agg_func)\n    \"\"\"\n    try:\n        client = await get_dirty_client_async()\n        result = await client.execute_async(\n            DATA_WORKER,\n            \"aggregate\",\n            data=data.data,\n            group_by=data.group_by,\n            agg_field=data.agg_field,\n            agg_func=data.agg_func,\n        )\n        return result\n    except DirtyError as e:\n        raise HTTPException(status_code=500, detail=str(e))\n\n\n@app.post(\"/api/data/etl\")\nasync def run_etl(data: ETLRequest):\n    \"\"\"\n    Run ETL pipeline with streaming progress.\n\n    Celery equivalent:\n        chain(extract.s(), transform.s(), load.s()).apply_async()\n    \"\"\"\n    async def generate():\n        try:\n            client = await get_dirty_client_async()\n            async for progress in client.stream_async(\n                DATA_WORKER,\n                \"etl_pipeline\",\n                source_data=data.source_data,\n                transformations=data.transformations,\n            ):\n                yield f\"data: {json.dumps(progress)}\\n\\n\"\n        except DirtyError as e:\n            yield f\"data: {json.dumps({'error': str(e)})}\\n\\n\"\n\n    return StreamingResponse(\n        generate(),\n        media_type=\"text/event-stream\",\n    )\n\n\n@app.post(\"/api/data/query\")\nasync def cached_query(data: QueryRequest):\n    \"\"\"Execute a cached query.\"\"\"\n    try:\n        client = await get_dirty_client_async()\n        result = await client.execute_async(\n            DATA_WORKER,\n            \"cached_query\",\n            query_key=data.query_key,\n            ttl=data.ttl,\n        )\n        return result\n    except DirtyError as e:\n        raise HTTPException(status_code=500, detail=str(e))\n\n\n@app.get(\"/api/data/stats\")\nasync def data_stats():\n    \"\"\"Get data worker statistics.\"\"\"\n    try:\n        client = await get_dirty_client_async()\n        result = await client.execute_async(DATA_WORKER, \"stats\")\n        return result\n    except DirtyError as e:\n        raise HTTPException(status_code=500, detail=str(e))\n\n\n# ============================================================================\n# Scheduled Tasks - Like Celery Beat tasks\n# ============================================================================\n\n@app.post(\"/api/scheduled/cleanup\")\nasync def run_cleanup(data: CleanupRequest = CleanupRequest()):\n    \"\"\"Run cleanup task (normally triggered by cron).\"\"\"\n    try:\n        client = await get_dirty_client_async()\n        result = await client.execute_async(\n            SCHEDULED_WORKER,\n            \"cleanup_old_files\",\n            directory=data.directory,\n            max_age_days=data.max_age_days,\n        )\n        return result\n    except DirtyError as e:\n        raise HTTPException(status_code=500, detail=str(e))\n\n\n@app.post(\"/api/scheduled/daily-report\")\nasync def run_daily_report():\n    \"\"\"Generate daily report.\"\"\"\n    try:\n        client = await get_dirty_client_async()\n        result = await client.execute_async(SCHEDULED_WORKER, \"generate_daily_report\")\n        return result\n    except DirtyError as e:\n        raise HTTPException(status_code=500, detail=str(e))\n\n\n@app.post(\"/api/scheduled/sync\")\nasync def run_sync(data: SyncRequest = SyncRequest()):\n    \"\"\"Sync external data.\"\"\"\n    try:\n        client = await get_dirty_client_async()\n        result = await client.execute_async(\n            SCHEDULED_WORKER,\n            \"sync_external_data\",\n            source=data.source,\n        )\n        return result\n    except DirtyError as e:\n        raise HTTPException(status_code=500, detail=str(e))\n\n\n@app.get(\"/api/scheduled/stats\")\nasync def scheduled_stats():\n    \"\"\"Get scheduled worker statistics.\"\"\"\n    try:\n        client = await get_dirty_client_async()\n        result = await client.execute_async(SCHEDULED_WORKER, \"stats\")\n        return result\n    except DirtyError as e:\n        raise HTTPException(status_code=500, detail=str(e))\n\n\n# ============================================================================\n# Health & Info Endpoints\n# ============================================================================\n\n@app.get(\"/\")\nasync def index():\n    \"\"\"API documentation.\"\"\"\n    return {\n        \"name\": \"Celery Replacement Demo\",\n        \"description\": \"Demonstrating Gunicorn dirty arbiters as Celery replacement (async ASGI)\",\n        \"docs\": \"/docs\",\n        \"endpoints\": {\n            \"email\": {\n                \"POST /api/email/send\": \"Send single email\",\n                \"POST /api/email/send-bulk\": \"Send bulk emails (streaming)\",\n                \"GET /api/email/stats\": \"Email worker stats\",\n            },\n            \"image\": {\n                \"POST /api/image/resize\": \"Resize image\",\n                \"POST /api/image/thumbnail\": \"Generate thumbnail\",\n                \"POST /api/image/process-batch\": \"Batch process (streaming)\",\n                \"GET /api/image/stats\": \"Image worker stats\",\n            },\n            \"data\": {\n                \"POST /api/data/aggregate\": \"Aggregate data\",\n                \"POST /api/data/etl\": \"Run ETL pipeline (streaming)\",\n                \"POST /api/data/query\": \"Cached query\",\n                \"GET /api/data/stats\": \"Data worker stats\",\n            },\n            \"scheduled\": {\n                \"POST /api/scheduled/cleanup\": \"Run cleanup\",\n                \"POST /api/scheduled/daily-report\": \"Generate report\",\n                \"POST /api/scheduled/sync\": \"Sync external data\",\n                \"GET /api/scheduled/stats\": \"Scheduled worker stats\",\n            },\n        },\n    }\n\n\n@app.get(\"/health\")\nasync def health():\n    \"\"\"Health check endpoint.\"\"\"\n    try:\n        client = await get_dirty_client_async()\n        # Quick ping to verify workers are running\n        await client.execute_async(EMAIL_WORKER, \"stats\")\n        return {\"status\": \"healthy\", \"workers\": \"connected\"}\n    except DirtyError:\n        raise HTTPException(\n            status_code=503,\n            detail={\"status\": \"degraded\", \"workers\": \"unavailable\"}\n        )\n"
  },
  {
    "path": "examples/celery_alternative/docker-compose.yml",
    "content": "# Docker Compose for Celery Replacement Example\n#\n# Notice: Only ONE service needed!\n# Compare with typical Celery deployment which requires:\n# - web (gunicorn/uvicorn)\n# - celery_worker\n# - celery_beat (for scheduled tasks)\n# - redis or rabbitmq\n#\n# With dirty arbiters, everything runs in a single container.\n\nservices:\n  app:\n    build:\n      context: ../..  # Gunicorn repo root\n      dockerfile: examples/celery_alternative/Dockerfile\n    ports:\n      - \"8003:8000\"\n    environment:\n      - GUNICORN_WORKERS=4\n      - DIRTY_WORKERS=9\n      - DIRTY_TIMEOUT=300\n      - LOG_LEVEL=info\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:8000/health\"]\n      interval: 30s\n      timeout: 10s\n      retries: 3\n      start_period: 10s\n    # Resource limits (optional)\n    deploy:\n      resources:\n        limits:\n          memory: 1G\n        reservations:\n          memory: 256M\n\n  # Test runner service\n  tests:\n    build:\n      context: ../..\n      dockerfile: examples/celery_alternative/Dockerfile\n    depends_on:\n      app:\n        condition: service_healthy\n    environment:\n      - APP_URL=http://app:8000\n    command: [\"python\", \"-m\", \"pytest\", \"tests/\", \"-v\", \"--tb=short\"]\n    profiles:\n      - test\n\n# For comparison, here's what a Celery deployment would look like:\n#\n# services:\n#   web:\n#     build: .\n#     command: gunicorn app:app -b 0.0.0.0:8000\n#     ports:\n#       - \"8000:8000\"\n#     depends_on:\n#       - redis\n#\n#   celery_worker:\n#     build: .\n#     command: celery -A tasks worker -l info\n#     depends_on:\n#       - redis\n#\n#   celery_beat:\n#     build: .\n#     command: celery -A tasks beat -l info\n#     depends_on:\n#       - redis\n#\n#   redis:\n#     image: redis:alpine\n#     ports:\n#       - \"6379:6379\"\n"
  },
  {
    "path": "examples/celery_alternative/gunicorn_conf.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nGunicorn Configuration - Celery Replacement Example\n\nThis configuration sets up:\n1. ASGI workers to handle web requests with async I/O (using uvloop)\n2. Dirty workers to handle background tasks (replacing Celery workers)\n\nWhy ASGI + Dirty Arbiters?\n- ASGI: Non-blocking HTTP handling - one worker handles many concurrent requests\n- Dirty: Stateful background workers - keep models/connections loaded in memory\n\nComparison with Celery deployment:\n- Celery: gunicorn app:app + celery -A tasks worker + redis-server\n- Dirty: gunicorn -c gunicorn_conf.py app:app (single command, no broker!)\n\"\"\"\n\nimport multiprocessing\nimport os\n\n# =============================================================================\n# Basic Settings\n# =============================================================================\n\n# Bind to all interfaces on port 8000\nbind = os.environ.get(\"GUNICORN_BIND\", \"0.0.0.0:8000\")\n\n# HTTP workers - handle incoming web requests\n# With ASGI, fewer workers needed since each handles many concurrent requests\nworkers = int(os.environ.get(\"GUNICORN_WORKERS\", min(4, multiprocessing.cpu_count() + 1)))\n\n# Use gunicorn's native ASGI worker for async support\n# This enables: await client.execute_async() without blocking\nworker_class = \"asgi\"\n\n# Use uvloop for better async performance\nasgi_loop = \"uvloop\"\n\n# Maximum concurrent connections per worker\nworker_connections = 1000\n\n# =============================================================================\n# Dirty Arbiter Settings (Celery Worker Replacement)\n# =============================================================================\n\n# Task workers - these replace Celery workers\n# Each dirty app can specify its own worker count via the `workers` class attribute\ndirty_apps = [\n    # Email tasks - 2 workers (I/O bound)\n    \"examples.celery_alternative.tasks:EmailWorker\",\n    # Image processing - 2 workers (CPU/memory intensive)\n    \"examples.celery_alternative.tasks:ImageWorker\",\n    # Data processing - 4 workers (parallelizable)\n    \"examples.celery_alternative.tasks:DataWorker\",\n    # Scheduled tasks - 1 worker\n    \"examples.celery_alternative.tasks:ScheduledWorker\",\n]\n\n# Total dirty workers (distributed among apps based on their `workers` attribute)\n# If not set, uses sum of all app worker counts\ndirty_workers = int(os.environ.get(\"DIRTY_WORKERS\", 9))  # 2+2+4+1 = 9\n\n# Task timeout in seconds (like Celery's task_time_limit)\ndirty_timeout = int(os.environ.get(\"DIRTY_TIMEOUT\", 300))\n\n# Threads per dirty worker (for concurrent task execution)\ndirty_threads = int(os.environ.get(\"DIRTY_THREADS\", 1))\n\n# Graceful shutdown timeout\ndirty_graceful_timeout = int(os.environ.get(\"DIRTY_GRACEFUL_TIMEOUT\", 30))\n\n# =============================================================================\n# Timeouts & Limits\n# =============================================================================\n\n# Worker timeout (seconds)\ntimeout = 120\n\n# Keep-alive connections\nkeepalive = 5\n\n# Maximum requests per worker before recycling\nmax_requests = 1000\nmax_requests_jitter = 50\n\n# =============================================================================\n# Logging\n# =============================================================================\n\n# Log level\nloglevel = os.environ.get(\"LOG_LEVEL\", \"info\")\n\n# Access log format\naccesslog = \"-\"\naccess_log_format = '%(h)s %(l)s %(u)s %(t)s \"%(r)s\" %(s)s %(b)s \"%(f)s\" \"%(a)s\" %(D)s'\n\n# Error log\nerrorlog = \"-\"\n\n# =============================================================================\n# Lifecycle Hooks\n# =============================================================================\n\ndef on_starting(server):\n    \"\"\"Called just before the master process is initialized.\"\"\"\n    print(\"=\" * 60)\n    print(\"Starting Gunicorn with Dirty Arbiters (Celery Replacement)\")\n    print(\"Using ASGI workers with uvloop for non-blocking HTTP handling\")\n    print(\"=\" * 60)\n\n\ndef on_dirty_starting(arbiter):\n    \"\"\"Called when the dirty arbiter is starting.\"\"\"\n    print(f\"[Dirty] Starting dirty arbiter\")\n    print(f\"[Dirty] Registered apps: {list(arbiter.cfg.dirty_apps)}\")\n\n\ndef dirty_post_fork(arbiter, worker):\n    \"\"\"Called after a dirty worker is forked.\"\"\"\n    print(f\"[Dirty] Worker {worker.pid} started\")\n\n\ndef dirty_worker_init(worker):\n    \"\"\"Called when a dirty worker initializes its apps.\"\"\"\n    print(f\"[Dirty] Worker {worker.pid} initialized apps: {list(worker.apps.keys())}\")\n\n\ndef dirty_worker_exit(arbiter, worker):\n    \"\"\"Called when a dirty worker exits.\"\"\"\n    print(f\"[Dirty] Worker {worker.pid} exiting\")\n\n\ndef worker_int(worker):\n    \"\"\"Called when a worker receives SIGINT.\"\"\"\n    print(f\"[HTTP] Worker {worker.pid} interrupted\")\n\n\ndef worker_exit(server, worker):\n    \"\"\"Called when a worker exits.\"\"\"\n    print(f\"[HTTP] Worker {worker.pid} exited\")\n\n\n# =============================================================================\n# Development vs Production\n# =============================================================================\n\n# Reload on code changes (development only)\nreload = os.environ.get(\"GUNICORN_RELOAD\", \"false\").lower() == \"true\"\n\n# Preload app for faster worker startup (production)\npreload_app = os.environ.get(\"GUNICORN_PRELOAD\", \"false\").lower() == \"true\"\n"
  },
  {
    "path": "examples/celery_alternative/requirements.txt",
    "content": "# Celery Replacement Example Dependencies\nfastapi>=0.109.0\nuvloop>=0.19.0\nhttpx>=0.26.0\npytest>=8.0.0\npytest-asyncio>=0.23.0\n"
  },
  {
    "path": "examples/celery_alternative/run_tests.sh",
    "content": "#!/bin/bash\n# Run tests for Celery Replacement example\n\nset -e\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nGUNICORN_ROOT=\"$(cd \"$SCRIPT_DIR/../..\" && pwd)\"\n\n# Add gunicorn to Python path\nexport PYTHONPATH=\"$GUNICORN_ROOT:$PYTHONPATH\"\n\ncd \"$SCRIPT_DIR\"\n\necho \"==========================================\"\necho \"Running Unit Tests\"\necho \"==========================================\"\npython -m pytest tests/test_tasks.py -v --tb=short\n\necho \"\"\necho \"==========================================\"\necho \"Unit tests passed!\"\necho \"==========================================\"\n\n# Check if integration tests should run\nif [ \"$1\" == \"--integration\" ] || [ \"$1\" == \"-i\" ]; then\n    APP_URL=\"${APP_URL:-http://localhost:8000}\"\n    echo \"\"\n    echo \"==========================================\"\n    echo \"Running Integration Tests against $APP_URL\"\n    echo \"==========================================\"\n    python -m pytest tests/test_integration.py -v --tb=short\nfi\n\necho \"\"\necho \"All tests completed successfully!\"\n"
  },
  {
    "path": "examples/celery_alternative/tasks.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nTask Workers - Celery Replacement using Gunicorn Dirty Arbiters\n\nThis module demonstrates how to replace Celery with Gunicorn's dirty arbiter\nfeature for background task processing. Key benefits:\n\n1. No external broker (Redis/RabbitMQ) needed - uses Unix sockets\n2. Stateful workers - maintain connections, models, caches across requests\n3. Integrated with your WSGI/ASGI app - no separate process management\n4. Streaming support for progress reporting\n5. Per-task-type worker allocation for memory optimization\n\nComparison with Celery:\n- Celery: @app.task decorator -> Dirty: DirtyApp class with methods\n- Celery: task.delay() -> Dirty: client.execute()\n- Celery: task.apply_async() -> Dirty: client.execute() with timeout\n- Celery: task progress -> Dirty: client.stream() with generators\n\"\"\"\n\nimport hashlib\nimport json\nimport os\nimport random\nimport smtplib\nimport time\nfrom datetime import datetime\nfrom email.mime.text import MIMEText\nfrom typing import Any, Generator\n\nfrom gunicorn.dirty.app import DirtyApp\n\n\nclass EmailWorker(DirtyApp):\n    \"\"\"\n    Email task worker - like Celery's @app.task for email sending.\n\n    Maintains SMTP connection pool across requests for efficiency.\n    In Celery, you'd create a new connection per task or manage it manually.\n    \"\"\"\n\n    # Limit to 2 workers since email sending is I/O bound\n    workers = 2\n\n    def __init__(self):\n        self.smtp_connection = None\n        self.emails_sent = 0\n        self.last_connected = None\n\n    def init(self):\n        \"\"\"Called once when worker starts - establish SMTP connection.\"\"\"\n        self._connect_smtp()\n\n    def _connect_smtp(self):\n        \"\"\"Establish SMTP connection (simulated for demo).\"\"\"\n        # In production, connect to real SMTP server:\n        # self.smtp_connection = smtplib.SMTP('smtp.example.com', 587)\n        # self.smtp_connection.starttls()\n        # self.smtp_connection.login(user, password)\n        self.last_connected = datetime.now().isoformat()\n        self.smtp_connection = \"connected\"  # Simulated\n\n    def __call__(self, action: str, *args, **kwargs) -> Any:\n        \"\"\"Dispatch to action methods.\"\"\"\n        method = getattr(self, action, None)\n        if method is None or action.startswith('_'):\n            raise ValueError(f\"Unknown action: {action}\")\n        return method(*args, **kwargs)\n\n    def send_email(self, to: str, subject: str, body: str,\n                   html: bool = False) -> dict:\n        \"\"\"\n        Send a single email.\n\n        Equivalent to Celery:\n            @app.task\n            def send_email(to, subject, body):\n                ...\n        \"\"\"\n        # Simulate email sending delay\n        time.sleep(random.uniform(0.1, 0.3))\n\n        self.emails_sent += 1\n\n        return {\n            \"status\": \"sent\",\n            \"to\": to,\n            \"subject\": subject,\n            \"message_id\": f\"msg-{self.emails_sent}-{int(time.time())}\",\n            \"timestamp\": datetime.now().isoformat(),\n        }\n\n    def send_bulk_emails(self, recipients: list, subject: str,\n                         body: str) -> Generator[dict, None, None]:\n        \"\"\"\n        Send bulk emails with progress streaming.\n\n        This is where dirty arbiters shine over Celery - real-time\n        progress without polling or WebSockets.\n\n        Equivalent to Celery:\n            @app.task(bind=True)\n            def send_bulk(self, recipients, subject, body):\n                for i, to in enumerate(recipients):\n                    send_email(to, subject, body)\n                    self.update_state(state='PROGRESS',\n                                      meta={'current': i, 'total': len(recipients)})\n        \"\"\"\n        total = len(recipients)\n        sent = 0\n        failed = 0\n\n        for i, to in enumerate(recipients):\n            try:\n                result = self.send_email(to, subject, body)\n                sent += 1\n                yield {\n                    \"type\": \"progress\",\n                    \"current\": i + 1,\n                    \"total\": total,\n                    \"percent\": int((i + 1) / total * 100),\n                    \"last_sent\": to,\n                    \"status\": \"sent\",\n                }\n            except Exception as e:\n                failed += 1\n                yield {\n                    \"type\": \"progress\",\n                    \"current\": i + 1,\n                    \"total\": total,\n                    \"percent\": int((i + 1) / total * 100),\n                    \"last_sent\": to,\n                    \"status\": \"failed\",\n                    \"error\": str(e),\n                }\n\n        # Final summary\n        yield {\n            \"type\": \"complete\",\n            \"total\": total,\n            \"sent\": sent,\n            \"failed\": failed,\n        }\n\n    def stats(self) -> dict:\n        \"\"\"Get worker statistics.\"\"\"\n        return {\n            \"emails_sent\": self.emails_sent,\n            \"smtp_connected\": self.smtp_connection is not None,\n            \"last_connected\": self.last_connected,\n            \"worker_pid\": os.getpid(),\n        }\n\n    def close(self):\n        \"\"\"Cleanup on shutdown.\"\"\"\n        if self.smtp_connection and self.smtp_connection != \"connected\":\n            self.smtp_connection.quit()\n\n\nclass ImageWorker(DirtyApp):\n    \"\"\"\n    Image processing worker - demonstrates CPU-intensive tasks.\n\n    Like Celery tasks for image resizing, thumbnails, watermarks.\n    Keeps image processing libraries loaded in memory.\n    \"\"\"\n\n    # Limit to 2 workers - image processing is memory intensive\n    workers = 2\n\n    def __init__(self):\n        self.pil_available = False\n        self.images_processed = 0\n\n    def init(self):\n        \"\"\"Load image processing libraries once at startup.\"\"\"\n        try:\n            # Try to import PIL - optional dependency\n            from PIL import Image\n            self.pil_available = True\n        except ImportError:\n            self.pil_available = False\n\n    def __call__(self, action: str, *args, **kwargs) -> Any:\n        method = getattr(self, action, None)\n        if method is None or action.startswith('_'):\n            raise ValueError(f\"Unknown action: {action}\")\n        return method(*args, **kwargs)\n\n    def resize(self, image_data: str, width: int, height: int) -> dict:\n        \"\"\"\n        Resize an image.\n\n        Equivalent to Celery:\n            @app.task\n            def resize_image(image_path, width, height):\n                img = Image.open(image_path)\n                img.thumbnail((width, height))\n                img.save(output_path)\n        \"\"\"\n        # Simulate image processing\n        time.sleep(random.uniform(0.2, 0.5))\n\n        self.images_processed += 1\n\n        # Create a fake \"processed\" result\n        # In production, image_data would be base64 decoded\n        data_size = len(image_data) if isinstance(image_data, str) else len(image_data)\n        result_hash = hashlib.md5(\n            f\"{data_size}{width}{height}\".encode()\n        ).hexdigest()[:16]\n\n        return {\n            \"status\": \"resized\",\n            \"original_size\": data_size,\n            \"target_dimensions\": f\"{width}x{height}\",\n            \"result_id\": f\"img-{result_hash}\",\n            \"pil_used\": self.pil_available,\n        }\n\n    def generate_thumbnail(self, image_data: str, size: int = 150) -> dict:\n        \"\"\"Generate a thumbnail.\"\"\"\n        return self.resize(image_data, size, size)\n\n    def process_batch(self, images: list, operation: str,\n                      **params) -> Generator[dict, None, None]:\n        \"\"\"\n        Process multiple images with progress streaming.\n        \"\"\"\n        total = len(images)\n\n        for i, img_info in enumerate(images):\n            try:\n                # Simulate fetching image data\n                image_data = img_info.get(\"data\", b\"fake_image_data\")\n\n                if operation == \"resize\":\n                    result = self.resize(\n                        image_data,\n                        params.get(\"width\", 800),\n                        params.get(\"height\", 600)\n                    )\n                elif operation == \"thumbnail\":\n                    result = self.generate_thumbnail(\n                        image_data,\n                        params.get(\"size\", 150)\n                    )\n                else:\n                    result = {\"error\": f\"Unknown operation: {operation}\"}\n\n                yield {\n                    \"type\": \"progress\",\n                    \"current\": i + 1,\n                    \"total\": total,\n                    \"percent\": int((i + 1) / total * 100),\n                    \"image_id\": img_info.get(\"id\", f\"img-{i}\"),\n                    \"result\": result,\n                }\n            except Exception as e:\n                yield {\n                    \"type\": \"error\",\n                    \"current\": i + 1,\n                    \"total\": total,\n                    \"image_id\": img_info.get(\"id\", f\"img-{i}\"),\n                    \"error\": str(e),\n                }\n\n        yield {\n            \"type\": \"complete\",\n            \"total\": total,\n            \"processed\": self.images_processed,\n        }\n\n    def stats(self) -> dict:\n        return {\n            \"images_processed\": self.images_processed,\n            \"pil_available\": self.pil_available,\n            \"worker_pid\": os.getpid(),\n        }\n\n\nclass DataWorker(DirtyApp):\n    \"\"\"\n    Data processing worker - demonstrates stateful data operations.\n\n    Maintains database connections, caches, and processing state.\n    Perfect for ETL tasks, report generation, data aggregation.\n    \"\"\"\n\n    # More workers for data tasks - they're often parallelizable\n    workers = 4\n\n    def __init__(self):\n        self.cache = {}\n        self.db_connection = None\n        self.tasks_completed = 0\n\n    def init(self):\n        \"\"\"Initialize database connection and cache.\"\"\"\n        # In production: self.db_connection = create_engine(DATABASE_URL)\n        self.db_connection = \"connected\"\n        self.cache = {}\n\n    def __call__(self, action: str, *args, **kwargs) -> Any:\n        method = getattr(self, action, None)\n        if method is None or action.startswith('_'):\n            raise ValueError(f\"Unknown action: {action}\")\n        return method(*args, **kwargs)\n\n    def aggregate(self, data: list, group_by: str,\n                  agg_field: str, agg_func: str = \"sum\") -> dict:\n        \"\"\"\n        Aggregate data - like a Celery task for report generation.\n\n        Equivalent to Celery:\n            @app.task\n            def aggregate_sales(data, group_by, agg_field):\n                df = pd.DataFrame(data)\n                return df.groupby(group_by)[agg_field].sum().to_dict()\n        \"\"\"\n        # Simulate aggregation\n        time.sleep(random.uniform(0.1, 0.3))\n\n        result = {}\n        for item in data:\n            key = item.get(group_by, \"unknown\")\n            value = item.get(agg_field, 0)\n\n            if key not in result:\n                if agg_func in (\"sum\", \"count\"):\n                    result[key] = 0\n                else:\n                    result[key] = []\n\n            if agg_func == \"sum\":\n                result[key] += value\n            elif agg_func == \"count\":\n                result[key] += 1\n            elif agg_func == \"list\":\n                result[key].append(value)\n\n        self.tasks_completed += 1\n\n        return {\n            \"status\": \"completed\",\n            \"group_by\": group_by,\n            \"agg_func\": agg_func,\n            \"result\": result,\n            \"record_count\": len(data),\n        }\n\n    def etl_pipeline(self, source_data: list,\n                     transformations: list) -> Generator[dict, None, None]:\n        \"\"\"\n        Run an ETL pipeline with progress streaming.\n\n        This replaces Celery chains/chords for multi-step processing:\n            chain(extract.s(), transform.s(), load.s())\n        \"\"\"\n        total_steps = len(transformations) + 2  # +2 for extract and load\n        current_step = 0\n        data = source_data\n\n        # Extract phase\n        yield {\n            \"type\": \"progress\",\n            \"phase\": \"extract\",\n            \"step\": current_step + 1,\n            \"total_steps\": total_steps,\n            \"message\": f\"Extracting {len(data)} records\",\n        }\n        time.sleep(0.2)  # Simulate extraction\n        current_step += 1\n\n        # Transform phases\n        for i, transform in enumerate(transformations):\n            transform_name = transform.get(\"name\", f\"transform_{i}\")\n            transform_type = transform.get(\"type\", \"passthrough\")\n\n            yield {\n                \"type\": \"progress\",\n                \"phase\": \"transform\",\n                \"step\": current_step + 1,\n                \"total_steps\": total_steps,\n                \"message\": f\"Applying {transform_name}\",\n            }\n\n            # Apply transformation\n            if transform_type == \"filter\":\n                field = transform.get(\"field\")\n                value = transform.get(\"value\")\n                data = [d for d in data if d.get(field) == value]\n            elif transform_type == \"map\":\n                field = transform.get(\"field\")\n                func = transform.get(\"func\", \"upper\")\n                for d in data:\n                    if field in d and isinstance(d[field], str):\n                        if func == \"upper\":\n                            d[field] = d[field].upper()\n                        elif func == \"lower\":\n                            d[field] = d[field].lower()\n\n            time.sleep(0.2)  # Simulate transformation\n            current_step += 1\n\n        # Load phase\n        yield {\n            \"type\": \"progress\",\n            \"phase\": \"load\",\n            \"step\": current_step + 1,\n            \"total_steps\": total_steps,\n            \"message\": f\"Loading {len(data)} records\",\n        }\n        time.sleep(0.2)  # Simulate loading\n\n        self.tasks_completed += 1\n\n        # Final result\n        yield {\n            \"type\": \"complete\",\n            \"records_processed\": len(source_data),\n            \"records_output\": len(data),\n            \"transformations_applied\": len(transformations),\n        }\n\n    def cached_query(self, query_key: str, ttl: int = 300) -> dict:\n        \"\"\"\n        Execute a cached query - demonstrates stateful caching.\n\n        Unlike Celery where you'd use Redis for caching,\n        the dirty worker maintains its own in-memory cache.\n        \"\"\"\n        now = time.time()\n\n        if query_key in self.cache:\n            cached = self.cache[query_key]\n            if now - cached[\"timestamp\"] < ttl:\n                return {\n                    \"status\": \"cache_hit\",\n                    \"data\": cached[\"data\"],\n                    \"cached_at\": cached[\"timestamp\"],\n                    \"age_seconds\": int(now - cached[\"timestamp\"]),\n                }\n\n        # Simulate query execution\n        time.sleep(random.uniform(0.2, 0.4))\n\n        # Generate fake result\n        result_data = {\n            \"query\": query_key,\n            \"rows\": random.randint(10, 100),\n            \"computed_at\": now,\n        }\n\n        self.cache[query_key] = {\n            \"data\": result_data,\n            \"timestamp\": now,\n        }\n\n        return {\n            \"status\": \"cache_miss\",\n            \"data\": result_data,\n            \"cached_at\": now,\n        }\n\n    def stats(self) -> dict:\n        return {\n            \"tasks_completed\": self.tasks_completed,\n            \"cache_size\": len(self.cache),\n            \"db_connected\": self.db_connection is not None,\n            \"worker_pid\": os.getpid(),\n        }\n\n    def close(self):\n        \"\"\"Cleanup on shutdown.\"\"\"\n        self.cache.clear()\n        if self.db_connection and self.db_connection != \"connected\":\n            self.db_connection.close()\n\n\nclass ScheduledWorker(DirtyApp):\n    \"\"\"\n    Scheduled task worker - for periodic/scheduled tasks.\n\n    While dirty arbiters don't have built-in scheduling like Celery Beat,\n    you can call these from a simple cron job or scheduler.\n    \"\"\"\n\n    workers = 1  # Single worker for scheduled tasks\n\n    def __init__(self):\n        self.last_runs = {}\n        self.run_counts = {}\n\n    def __call__(self, action: str, *args, **kwargs) -> Any:\n        method = getattr(self, action, None)\n        if method is None or action.startswith('_'):\n            raise ValueError(f\"Unknown action: {action}\")\n\n        # Track runs\n        self.last_runs[action] = datetime.now().isoformat()\n        self.run_counts[action] = self.run_counts.get(action, 0) + 1\n\n        return method(*args, **kwargs)\n\n    def cleanup_old_files(self, directory: str, max_age_days: int = 7) -> dict:\n        \"\"\"\n        Cleanup old files - like a Celery periodic task.\n\n        Equivalent to Celery Beat:\n            @app.task\n            def cleanup():\n                ...\n\n            app.conf.beat_schedule = {\n                'cleanup-every-hour': {\n                    'task': 'tasks.cleanup',\n                    'schedule': 3600.0,\n                },\n            }\n        \"\"\"\n        # Simulate cleanup\n        time.sleep(0.3)\n\n        files_deleted = random.randint(0, 10)\n\n        return {\n            \"status\": \"completed\",\n            \"directory\": directory,\n            \"files_deleted\": files_deleted,\n            \"space_freed_mb\": files_deleted * random.uniform(0.1, 5.0),\n        }\n\n    def generate_daily_report(self) -> dict:\n        \"\"\"Generate daily report.\"\"\"\n        time.sleep(0.5)\n\n        return {\n            \"status\": \"completed\",\n            \"report_date\": datetime.now().strftime(\"%Y-%m-%d\"),\n            \"metrics\": {\n                \"active_users\": random.randint(100, 1000),\n                \"new_signups\": random.randint(10, 50),\n                \"revenue\": random.uniform(1000, 10000),\n            },\n        }\n\n    def sync_external_data(self, source: str) -> dict:\n        \"\"\"Sync data from external source.\"\"\"\n        time.sleep(0.4)\n\n        return {\n            \"status\": \"completed\",\n            \"source\": source,\n            \"records_synced\": random.randint(50, 500),\n            \"sync_time\": datetime.now().isoformat(),\n        }\n\n    def stats(self) -> dict:\n        return {\n            \"last_runs\": self.last_runs,\n            \"run_counts\": self.run_counts,\n            \"worker_pid\": os.getpid(),\n        }\n"
  },
  {
    "path": "examples/celery_alternative/tests/__init__.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n# Tests package\n"
  },
  {
    "path": "examples/celery_alternative/tests/conftest.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nPytest configuration for Celery Replacement tests.\n\"\"\"\n\nimport sys\nfrom pathlib import Path\n\n# Add gunicorn source to path for imports\ngunicorn_root = Path(__file__).parent.parent.parent.parent\nsys.path.insert(0, str(gunicorn_root))\n"
  },
  {
    "path": "examples/celery_alternative/tests/test_integration.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nIntegration Tests for Celery Replacement Example\n\nThese tests run against the full application with Gunicorn and dirty arbiters.\nThey can be run locally or in Docker.\n\nUsage:\n    # Local (with gunicorn running):\n    APP_URL=http://localhost:8000 pytest tests/test_integration.py -v\n\n    # Docker:\n    docker compose --profile test up --build --abort-on-container-exit\n\"\"\"\n\nimport json\nimport os\nimport time\n\nimport pytest\nimport requests\n\n# Get app URL from environment or use default\nAPP_URL = os.environ.get(\"APP_URL\", \"http://localhost:8000\")\n\n\ndef read_sse_events(response, max_events=100):\n    \"\"\"\n    Read SSE events from a streaming response.\n\n    Stops when receiving a 'complete' or 'error' event, or max_events reached.\n    \"\"\"\n    events = []\n    for line in response.iter_lines(decode_unicode=True):\n        if line.startswith(\"data: \"):\n            data = json.loads(line[6:])\n            events.append(data)\n            if data.get(\"type\") in (\"complete\", \"error\"):\n                break\n            if len(events) >= max_events:\n                break\n    return events\n\n\ndef wait_for_app(timeout=30):\n    \"\"\"Wait for the application to be ready.\"\"\"\n    start = time.time()\n    while time.time() - start < timeout:\n        try:\n            resp = requests.get(f\"{APP_URL}/health\", timeout=5)\n            if resp.status_code == 200:\n                return True\n        except requests.exceptions.ConnectionError:\n            pass\n        time.sleep(1)\n    return False\n\n\n@pytest.fixture(scope=\"module\", autouse=True)\ndef ensure_app_running():\n    \"\"\"Ensure the application is running before tests.\"\"\"\n    if not wait_for_app():\n        pytest.skip(\"Application not available\")\n\n\nclass TestHealthEndpoint:\n    \"\"\"Test health check endpoint.\"\"\"\n\n    def test_health_check(self):\n        \"\"\"Test that health endpoint returns healthy status.\"\"\"\n        resp = requests.get(f\"{APP_URL}/health\")\n        assert resp.status_code == 200\n\n        data = resp.json()\n        assert data[\"status\"] == \"healthy\"\n        assert data[\"workers\"] == \"connected\"\n\n\nclass TestEmailTasks:\n    \"\"\"Integration tests for email tasks.\"\"\"\n\n    def test_send_single_email(self):\n        \"\"\"Test sending a single email via API.\"\"\"\n        resp = requests.post(\n            f\"{APP_URL}/api/email/send\",\n            json={\n                \"to\": \"test@example.com\",\n                \"subject\": \"Integration Test\",\n                \"body\": \"Hello from integration test\",\n            },\n        )\n        assert resp.status_code == 200\n\n        data = resp.json()\n        assert data[\"status\"] == \"sent\"\n        assert data[\"to\"] == \"test@example.com\"\n        assert \"message_id\" in data\n\n    def test_send_bulk_emails_streaming(self):\n        \"\"\"Test bulk email sending with SSE streaming.\"\"\"\n        recipients = [\"a@test.com\", \"b@test.com\", \"c@test.com\"]\n\n        resp = requests.post(\n            f\"{APP_URL}/api/email/send-bulk\",\n            json={\n                \"recipients\": recipients,\n                \"subject\": \"Bulk Test\",\n                \"body\": \"Hello all\",\n            },\n            stream=True,\n        )\n        assert resp.status_code == 200\n\n        events = read_sse_events(resp)\n\n        # Should have progress for each recipient + complete\n        assert len(events) == len(recipients) + 1\n\n        # Check progress events\n        for i, event in enumerate(events[:-1]):\n            assert event[\"type\"] == \"progress\"\n            assert event[\"current\"] == i + 1\n\n        # Check complete event\n        assert events[-1][\"type\"] == \"complete\"\n        assert events[-1][\"sent\"] == len(recipients)\n\n    def test_email_stats(self):\n        \"\"\"Test email worker statistics endpoint.\"\"\"\n        # Send an email first\n        requests.post(\n            f\"{APP_URL}/api/email/send\",\n            json={\"to\": \"x@x.com\", \"subject\": \"S\", \"body\": \"B\"},\n        )\n\n        resp = requests.get(f\"{APP_URL}/api/email/stats\")\n        assert resp.status_code == 200\n\n        data = resp.json()\n        assert data[\"emails_sent\"] >= 1\n        assert data[\"smtp_connected\"] is True\n        assert \"worker_pid\" in data\n\n\nclass TestImageTasks:\n    \"\"\"Integration tests for image tasks.\"\"\"\n\n    def test_resize_image(self):\n        \"\"\"Test image resizing via API.\"\"\"\n        resp = requests.post(\n            f\"{APP_URL}/api/image/resize\",\n            json={\n                \"image_data\": \"base64_encoded_image_data\",\n                \"width\": 800,\n                \"height\": 600,\n            },\n        )\n        assert resp.status_code == 200\n\n        data = resp.json()\n        assert data[\"status\"] == \"resized\"\n        assert data[\"target_dimensions\"] == \"800x600\"\n\n    def test_generate_thumbnail(self):\n        \"\"\"Test thumbnail generation via API.\"\"\"\n        resp = requests.post(\n            f\"{APP_URL}/api/image/thumbnail\",\n            json={\n                \"image_data\": \"base64_image\",\n                \"size\": 150,\n            },\n        )\n        assert resp.status_code == 200\n\n        data = resp.json()\n        assert data[\"status\"] == \"resized\"\n        assert data[\"target_dimensions\"] == \"150x150\"\n\n    def test_batch_processing_streaming(self):\n        \"\"\"Test batch image processing with streaming.\"\"\"\n        images = [\n            {\"id\": \"img1\", \"data\": \"data1\"},\n            {\"id\": \"img2\", \"data\": \"data2\"},\n        ]\n\n        resp = requests.post(\n            f\"{APP_URL}/api/image/process-batch\",\n            json={\n                \"images\": images,\n                \"operation\": \"resize\",\n                \"width\": 400,\n                \"height\": 300,\n            },\n            stream=True,\n        )\n        assert resp.status_code == 200\n\n        events = read_sse_events(resp)\n\n        assert len(events) == len(images) + 1\n        assert events[-1][\"type\"] == \"complete\"\n\n    def test_image_stats(self):\n        \"\"\"Test image worker statistics.\"\"\"\n        resp = requests.get(f\"{APP_URL}/api/image/stats\")\n        assert resp.status_code == 200\n\n        data = resp.json()\n        assert \"images_processed\" in data\n        assert \"worker_pid\" in data\n\n\nclass TestDataTasks:\n    \"\"\"Integration tests for data processing tasks.\"\"\"\n\n    def test_aggregate_data(self):\n        \"\"\"Test data aggregation via API.\"\"\"\n        resp = requests.post(\n            f\"{APP_URL}/api/data/aggregate\",\n            json={\n                \"data\": [\n                    {\"category\": \"A\", \"value\": 10},\n                    {\"category\": \"B\", \"value\": 20},\n                    {\"category\": \"A\", \"value\": 30},\n                ],\n                \"group_by\": \"category\",\n                \"agg_field\": \"value\",\n                \"agg_func\": \"sum\",\n            },\n        )\n        assert resp.status_code == 200\n\n        data = resp.json()\n        assert data[\"status\"] == \"completed\"\n        assert data[\"result\"][\"A\"] == 40\n        assert data[\"result\"][\"B\"] == 20\n\n    def test_etl_pipeline_streaming(self):\n        \"\"\"Test ETL pipeline with streaming progress.\"\"\"\n        resp = requests.post(\n            f\"{APP_URL}/api/data/etl\",\n            json={\n                \"source_data\": [\n                    {\"name\": \"alice\", \"status\": \"active\"},\n                    {\"name\": \"bob\", \"status\": \"inactive\"},\n                    {\"name\": \"charlie\", \"status\": \"active\"},\n                ],\n                \"transformations\": [\n                    {\"name\": \"filter\", \"type\": \"filter\",\n                     \"field\": \"status\", \"value\": \"active\"},\n                ],\n            },\n            stream=True,\n        )\n        assert resp.status_code == 200\n\n        events = read_sse_events(resp)\n\n        # extract + transform + load + complete\n        assert len(events) == 4\n\n        # Check phases\n        phases = [e.get(\"phase\") for e in events[:-1]]\n        assert \"extract\" in phases\n        assert \"transform\" in phases\n        assert \"load\" in phases\n\n        # Final result\n        assert events[-1][\"type\"] == \"complete\"\n        assert events[-1][\"records_output\"] == 2\n\n    def test_cached_query(self):\n        \"\"\"Test cached query functionality.\"\"\"\n        query_key = f\"test_query_{time.time()}\"\n\n        # First call - cache miss\n        resp1 = requests.post(\n            f\"{APP_URL}/api/data/query\",\n            json={\"query_key\": query_key, \"ttl\": 300},\n        )\n        assert resp1.status_code == 200\n        assert resp1.json()[\"status\"] == \"cache_miss\"\n\n        # Second call - may be cache hit or miss depending on which worker handles it\n        # (cache is per-worker, not shared)\n        # Retry a few times to likely hit the same worker\n        cache_hit = False\n        for _ in range(5):\n            resp2 = requests.post(\n                f\"{APP_URL}/api/data/query\",\n                json={\"query_key\": query_key, \"ttl\": 300},\n            )\n            assert resp2.status_code == 200\n            if resp2.json()[\"status\"] == \"cache_hit\":\n                cache_hit = True\n                break\n        assert cache_hit, \"Expected cache_hit after multiple requests to same key\"\n\n    def test_data_stats(self):\n        \"\"\"Test data worker statistics.\"\"\"\n        resp = requests.get(f\"{APP_URL}/api/data/stats\")\n        assert resp.status_code == 200\n\n        data = resp.json()\n        assert \"tasks_completed\" in data\n        assert \"cache_size\" in data\n\n\nclass TestScheduledTasks:\n    \"\"\"Integration tests for scheduled tasks.\"\"\"\n\n    def test_cleanup_task(self):\n        \"\"\"Test cleanup task execution.\"\"\"\n        resp = requests.post(\n            f\"{APP_URL}/api/scheduled/cleanup\",\n            json={\"directory\": \"/tmp/test\", \"max_age_days\": 7},\n        )\n        assert resp.status_code == 200\n\n        data = resp.json()\n        assert data[\"status\"] == \"completed\"\n        assert \"files_deleted\" in data\n\n    def test_daily_report(self):\n        \"\"\"Test daily report generation.\"\"\"\n        resp = requests.post(f\"{APP_URL}/api/scheduled/daily-report\")\n        assert resp.status_code == 200\n\n        data = resp.json()\n        assert data[\"status\"] == \"completed\"\n        assert \"metrics\" in data\n\n    def test_sync_task(self):\n        \"\"\"Test data sync task.\"\"\"\n        resp = requests.post(\n            f\"{APP_URL}/api/scheduled/sync\",\n            json={\"source\": \"test_source\"},\n        )\n        assert resp.status_code == 200\n\n        data = resp.json()\n        assert data[\"status\"] == \"completed\"\n        assert data[\"source\"] == \"test_source\"\n\n    def test_scheduled_stats(self):\n        \"\"\"Test scheduled worker statistics.\"\"\"\n        # Run a task first\n        requests.post(f\"{APP_URL}/api/scheduled/daily-report\")\n\n        resp = requests.get(f\"{APP_URL}/api/scheduled/stats\")\n        assert resp.status_code == 200\n\n        data = resp.json()\n        assert \"run_counts\" in data\n        assert \"generate_daily_report\" in data[\"run_counts\"]\n\n\nclass TestConcurrency:\n    \"\"\"Test concurrent task execution.\"\"\"\n\n    def test_concurrent_requests(self):\n        \"\"\"Test that multiple concurrent requests are handled.\"\"\"\n        import concurrent.futures\n\n        def send_email():\n            return requests.post(\n                f\"{APP_URL}/api/email/send\",\n                json={\"to\": \"x@x.com\", \"subject\": \"Concurrent\", \"body\": \"Test\"},\n            )\n\n        # Send 10 concurrent requests\n        with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:\n            futures = [executor.submit(send_email) for _ in range(10)]\n            results = [f.result() for f in futures]\n\n        # All should succeed\n        assert all(r.status_code == 200 for r in results)\n        assert all(r.json()[\"status\"] == \"sent\" for r in results)\n\n    def test_mixed_task_types(self):\n        \"\"\"Test different task types running concurrently.\"\"\"\n        import concurrent.futures\n\n        def email_task():\n            return requests.post(\n                f\"{APP_URL}/api/email/send\",\n                json={\"to\": \"x@x.com\", \"subject\": \"S\", \"body\": \"B\"},\n            )\n\n        def image_task():\n            return requests.post(\n                f\"{APP_URL}/api/image/resize\",\n                json={\"image_data\": \"x\", \"width\": 100, \"height\": 100},\n            )\n\n        def data_task():\n            return requests.post(\n                f\"{APP_URL}/api/data/aggregate\",\n                json={\n                    \"data\": [{\"a\": 1}],\n                    \"group_by\": \"a\",\n                    \"agg_field\": \"a\",\n                    \"agg_func\": \"sum\",\n                },\n            )\n\n        with concurrent.futures.ThreadPoolExecutor(max_workers=9) as executor:\n            futures = []\n            for _ in range(3):\n                futures.append(executor.submit(email_task))\n                futures.append(executor.submit(image_task))\n                futures.append(executor.submit(data_task))\n\n            results = [f.result() for f in futures]\n\n        # All should succeed\n        assert all(r.status_code == 200 for r in results)\n\n\nclass TestErrorHandling:\n    \"\"\"Test error handling scenarios.\"\"\"\n\n    def test_invalid_action(self):\n        \"\"\"Test that invalid actions return appropriate errors.\"\"\"\n        # This would require modifying the API to expose raw execute\n        # For now, we test via a malformed request\n        resp = requests.post(\n            f\"{APP_URL}/api/email/send\",\n            json={},  # Missing required fields\n        )\n        # Should get a validation error (FastAPI returns 422)\n        assert resp.status_code == 422\n"
  },
  {
    "path": "examples/celery_alternative/tests/test_tasks.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nUnit Tests for Task Workers\n\nThese tests verify the task worker logic without running Gunicorn.\nThey test the DirtyApp classes directly.\n\"\"\"\n\nimport pytest\nfrom examples.celery_alternative.tasks import (\n    EmailWorker,\n    ImageWorker,\n    DataWorker,\n    ScheduledWorker,\n)\n\n\nclass TestEmailWorker:\n    \"\"\"Tests for EmailWorker task class.\"\"\"\n\n    def setup_method(self):\n        \"\"\"Set up test fixtures.\"\"\"\n        self.worker = EmailWorker()\n        self.worker.init()\n\n    def test_send_email(self):\n        \"\"\"Test sending a single email.\"\"\"\n        result = self.worker(\"send_email\",\n                             to=\"test@example.com\",\n                             subject=\"Test\",\n                             body=\"Hello\")\n\n        assert result[\"status\"] == \"sent\"\n        assert result[\"to\"] == \"test@example.com\"\n        assert result[\"subject\"] == \"Test\"\n        assert \"message_id\" in result\n        assert \"timestamp\" in result\n\n    def test_send_email_increments_counter(self):\n        \"\"\"Test that email counter increments.\"\"\"\n        initial_count = self.worker.emails_sent\n\n        self.worker(\"send_email\", to=\"a@x.com\", subject=\"S\", body=\"B\")\n        self.worker(\"send_email\", to=\"b@x.com\", subject=\"S\", body=\"B\")\n\n        assert self.worker.emails_sent == initial_count + 2\n\n    def test_send_bulk_emails_streaming(self):\n        \"\"\"Test bulk email sending with progress streaming.\"\"\"\n        recipients = [\"a@x.com\", \"b@x.com\", \"c@x.com\"]\n\n        results = list(self.worker(\"send_bulk_emails\",\n                                   recipients=recipients,\n                                   subject=\"Bulk\",\n                                   body=\"Hello all\"))\n\n        # Should have progress updates + final complete\n        assert len(results) == len(recipients) + 1\n\n        # Check progress updates\n        for i, r in enumerate(results[:-1]):\n            assert r[\"type\"] == \"progress\"\n            assert r[\"current\"] == i + 1\n            assert r[\"total\"] == len(recipients)\n\n        # Check final result\n        final = results[-1]\n        assert final[\"type\"] == \"complete\"\n        assert final[\"total\"] == len(recipients)\n        assert final[\"sent\"] == len(recipients)\n\n    def test_stats(self):\n        \"\"\"Test worker statistics.\"\"\"\n        self.worker(\"send_email\", to=\"x@x.com\", subject=\"S\", body=\"B\")\n\n        stats = self.worker(\"stats\")\n\n        assert stats[\"emails_sent\"] >= 1\n        assert stats[\"smtp_connected\"] is True\n        assert \"worker_pid\" in stats\n\n    def test_unknown_action_raises(self):\n        \"\"\"Test that unknown actions raise ValueError.\"\"\"\n        with pytest.raises(ValueError, match=\"Unknown action\"):\n            self.worker(\"nonexistent_action\")\n\n    def test_private_method_raises(self):\n        \"\"\"Test that private methods cannot be called.\"\"\"\n        with pytest.raises(ValueError, match=\"Unknown action\"):\n            self.worker(\"_connect_smtp\")\n\n\nclass TestImageWorker:\n    \"\"\"Tests for ImageWorker task class.\"\"\"\n\n    def setup_method(self):\n        \"\"\"Set up test fixtures.\"\"\"\n        self.worker = ImageWorker()\n        self.worker.init()\n\n    def test_resize_image(self):\n        \"\"\"Test image resizing.\"\"\"\n        result = self.worker(\"resize\",\n                             image_data=\"fake_image_data\",\n                             width=800,\n                             height=600)\n\n        assert result[\"status\"] == \"resized\"\n        assert result[\"target_dimensions\"] == \"800x600\"\n        assert \"result_id\" in result\n\n    def test_generate_thumbnail(self):\n        \"\"\"Test thumbnail generation.\"\"\"\n        result = self.worker(\"generate_thumbnail\",\n                             image_data=\"fake_image_data\",\n                             size=150)\n\n        assert result[\"status\"] == \"resized\"\n        assert result[\"target_dimensions\"] == \"150x150\"\n\n    def test_process_batch_streaming(self):\n        \"\"\"Test batch processing with progress streaming.\"\"\"\n        images = [\n            {\"id\": \"img1\", \"data\": b\"data1\"},\n            {\"id\": \"img2\", \"data\": b\"data2\"},\n            {\"id\": \"img3\", \"data\": b\"data3\"},\n        ]\n\n        results = list(self.worker(\"process_batch\",\n                                   images=images,\n                                   operation=\"resize\",\n                                   width=800,\n                                   height=600))\n\n        # Progress for each image + complete\n        assert len(results) == len(images) + 1\n\n        # Check progress updates\n        for i, r in enumerate(results[:-1]):\n            assert r[\"type\"] == \"progress\"\n            assert r[\"image_id\"] == f\"img{i+1}\"\n            assert \"result\" in r\n\n        # Check final result\n        final = results[-1]\n        assert final[\"type\"] == \"complete\"\n\n    def test_stats(self):\n        \"\"\"Test worker statistics.\"\"\"\n        self.worker(\"resize\", image_data=b\"x\", width=100, height=100)\n\n        stats = self.worker(\"stats\")\n\n        assert stats[\"images_processed\"] >= 1\n        assert \"pil_available\" in stats\n        assert \"worker_pid\" in stats\n\n\nclass TestDataWorker:\n    \"\"\"Tests for DataWorker task class.\"\"\"\n\n    def setup_method(self):\n        \"\"\"Set up test fixtures.\"\"\"\n        self.worker = DataWorker()\n        self.worker.init()\n\n    def test_aggregate_sum(self):\n        \"\"\"Test data aggregation with sum.\"\"\"\n        data = [\n            {\"category\": \"A\", \"value\": 10},\n            {\"category\": \"B\", \"value\": 20},\n            {\"category\": \"A\", \"value\": 30},\n        ]\n\n        result = self.worker(\"aggregate\",\n                             data=data,\n                             group_by=\"category\",\n                             agg_field=\"value\",\n                             agg_func=\"sum\")\n\n        assert result[\"status\"] == \"completed\"\n        assert result[\"result\"][\"A\"] == 40\n        assert result[\"result\"][\"B\"] == 20\n\n    def test_aggregate_count(self):\n        \"\"\"Test data aggregation with count.\"\"\"\n        data = [\n            {\"category\": \"A\", \"value\": 10},\n            {\"category\": \"B\", \"value\": 20},\n            {\"category\": \"A\", \"value\": 30},\n        ]\n\n        result = self.worker(\"aggregate\",\n                             data=data,\n                             group_by=\"category\",\n                             agg_field=\"value\",\n                             agg_func=\"count\")\n\n        assert result[\"result\"][\"A\"] == 2\n        assert result[\"result\"][\"B\"] == 1\n\n    def test_etl_pipeline_streaming(self):\n        \"\"\"Test ETL pipeline with progress streaming.\"\"\"\n        source_data = [\n            {\"name\": \"alice\", \"status\": \"active\"},\n            {\"name\": \"bob\", \"status\": \"inactive\"},\n            {\"name\": \"charlie\", \"status\": \"active\"},\n        ]\n        transformations = [\n            {\"name\": \"filter_active\", \"type\": \"filter\",\n             \"field\": \"status\", \"value\": \"active\"},\n            {\"name\": \"uppercase\", \"type\": \"map\",\n             \"field\": \"name\", \"func\": \"upper\"},\n        ]\n\n        results = list(self.worker(\"etl_pipeline\",\n                                   source_data=source_data,\n                                   transformations=transformations))\n\n        # extract + transforms + load + complete\n        expected_steps = 1 + len(transformations) + 1 + 1\n        assert len(results) == expected_steps\n\n        # Check phases\n        assert results[0][\"phase\"] == \"extract\"\n        assert results[1][\"phase\"] == \"transform\"\n        assert results[2][\"phase\"] == \"transform\"\n        assert results[3][\"phase\"] == \"load\"\n        assert results[4][\"type\"] == \"complete\"\n\n        # Final should have 2 records (filtered)\n        assert results[4][\"records_output\"] == 2\n\n    def test_cached_query_miss_then_hit(self):\n        \"\"\"Test query caching - miss then hit.\"\"\"\n        # First call - cache miss\n        result1 = self.worker(\"cached_query\", query_key=\"test_query\", ttl=300)\n        assert result1[\"status\"] == \"cache_miss\"\n\n        # Second call - cache hit\n        result2 = self.worker(\"cached_query\", query_key=\"test_query\", ttl=300)\n        assert result2[\"status\"] == \"cache_hit\"\n\n    def test_stats(self):\n        \"\"\"Test worker statistics.\"\"\"\n        self.worker(\"aggregate\",\n                    data=[{\"a\": 1}],\n                    group_by=\"a\",\n                    agg_field=\"a\")\n\n        stats = self.worker(\"stats\")\n\n        assert stats[\"tasks_completed\"] >= 1\n        assert \"cache_size\" in stats\n        assert stats[\"db_connected\"] is True\n\n\nclass TestScheduledWorker:\n    \"\"\"Tests for ScheduledWorker task class.\"\"\"\n\n    def setup_method(self):\n        \"\"\"Set up test fixtures.\"\"\"\n        self.worker = ScheduledWorker()\n\n    def test_cleanup_old_files(self):\n        \"\"\"Test file cleanup task.\"\"\"\n        result = self.worker(\"cleanup_old_files\",\n                             directory=\"/tmp/test\",\n                             max_age_days=7)\n\n        assert result[\"status\"] == \"completed\"\n        assert result[\"directory\"] == \"/tmp/test\"\n        assert \"files_deleted\" in result\n        assert \"space_freed_mb\" in result\n\n    def test_generate_daily_report(self):\n        \"\"\"Test daily report generation.\"\"\"\n        result = self.worker(\"generate_daily_report\")\n\n        assert result[\"status\"] == \"completed\"\n        assert \"report_date\" in result\n        assert \"metrics\" in result\n        assert \"active_users\" in result[\"metrics\"]\n        assert \"new_signups\" in result[\"metrics\"]\n        assert \"revenue\" in result[\"metrics\"]\n\n    def test_sync_external_data(self):\n        \"\"\"Test external data sync.\"\"\"\n        result = self.worker(\"sync_external_data\", source=\"test_api\")\n\n        assert result[\"status\"] == \"completed\"\n        assert result[\"source\"] == \"test_api\"\n        assert \"records_synced\" in result\n\n    def test_stats_tracks_runs(self):\n        \"\"\"Test that stats tracks task runs.\"\"\"\n        self.worker(\"cleanup_old_files\", directory=\"/tmp\", max_age_days=1)\n        self.worker(\"cleanup_old_files\", directory=\"/tmp\", max_age_days=1)\n        self.worker(\"generate_daily_report\")\n\n        stats = self.worker(\"stats\")\n\n        assert stats[\"run_counts\"][\"cleanup_old_files\"] == 2\n        assert stats[\"run_counts\"][\"generate_daily_report\"] == 1\n        assert \"cleanup_old_files\" in stats[\"last_runs\"]\n"
  },
  {
    "path": "examples/deep/__init__.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n"
  },
  {
    "path": "examples/deep/test.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n#\n# Example code from Eventlet sources\n\nfrom wsgiref.validate import validator\n\nfrom gunicorn import __version__\n\n\n@validator\ndef app(environ, start_response):\n    \"\"\"Simplest possible application object\"\"\"\n\n    data = b'Hello, World!\\n'\n    status = '200 OK'\n\n    response_headers = [\n        ('Content-type', 'text/plain'),\n        ('Content-Length', str(len(data))),\n        ('X-Gunicorn-Version', __version__),\n        ('Foo', 'B\\u00e5r'),  # Foo: Bår\n    ]\n    start_response(status, response_headers)\n    return iter([data])\n"
  },
  {
    "path": "examples/dirty_example/Dockerfile",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nFROM python:3.12-slim\n\nWORKDIR /app\n\n# Copy gunicorn source\nCOPY . /app/gunicorn-src\n\n# Install gunicorn and dependencies\n# setproctitle is needed for process title changes\nRUN pip install --no-cache-dir /app/gunicorn-src setproctitle\n\n# Copy example files\nCOPY examples/dirty_example/ /app/examples/dirty_example/\n\nWORKDIR /app\n\n# Expose the port\nEXPOSE 8000\n\n# Default command - run the example tests\nCMD [\"python\", \"-m\", \"pytest\", \"-v\", \"examples/dirty_example/\"]\n"
  },
  {
    "path": "examples/dirty_example/__init__.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n"
  },
  {
    "path": "examples/dirty_example/dirty_app.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nExample Dirty Application - Simulates ML Model Loading and Inference\n\nThis demonstrates how to create a DirtyApp that:\n1. Loads \"models\" at startup (init)\n2. Handles requests from HTTP workers (__call__)\n3. Cleans up on shutdown (close)\n\"\"\"\n\nimport os\nimport time\nimport hashlib\nfrom gunicorn.dirty.app import DirtyApp\nfrom gunicorn.dirty import stash\n\n\nclass MLApp(DirtyApp):\n    \"\"\"\n    Example dirty application that simulates ML model operations.\n\n    In a real application, this would load actual ML models like:\n    - PyTorch models\n    - TensorFlow models\n    - Scikit-learn models\n    - LLM models (Hugging Face, etc.)\n    \"\"\"\n\n    def __init__(self):\n        self.models = {}\n        self.load_count = 0\n        self.inference_count = 0\n\n    def init(self):\n        \"\"\"Called once when dirty worker starts.\"\"\"\n        print(f\"[MLApp] Initializing... (pid: {__import__('os').getpid()})\")\n        # Simulate loading a default model (takes time)\n        self._load_model(\"default\")\n        print(f\"[MLApp] Initialization complete. Models loaded: {list(self.models.keys())}\")\n\n    def __call__(self, action, *args, **kwargs):\n        \"\"\"Dispatch to action methods.\"\"\"\n        method = getattr(self, action, None)\n        if method is None or action.startswith('_'):\n            raise ValueError(f\"Unknown action: {action}\")\n        return method(*args, **kwargs)\n\n    def _load_model(self, name):\n        \"\"\"Simulate loading a model (expensive operation).\"\"\"\n        print(f\"[MLApp] Loading model '{name}'...\")\n        # Simulate model loading time\n        time.sleep(0.5)\n        # Create a fake \"model\" object\n        self.models[name] = {\n            \"name\": name,\n            \"loaded_at\": time.time(),\n            \"version\": \"1.0.0\",\n            \"parameters\": 1_000_000,  # Simulated parameter count\n        }\n        self.load_count += 1\n        print(f\"[MLApp] Model '{name}' loaded successfully\")\n        return self.models[name]\n\n    def load_model(self, name):\n        \"\"\"Load a model into memory (called from HTTP workers).\"\"\"\n        if name in self.models:\n            return {\"status\": \"already_loaded\", \"model\": self.models[name]}\n\n        model = self._load_model(name)\n        return {\"status\": \"loaded\", \"model\": model}\n\n    def list_models(self):\n        \"\"\"List all loaded models.\"\"\"\n        return {\n            \"models\": list(self.models.keys()),\n            \"count\": len(self.models),\n            \"total_loads\": self.load_count,\n            \"total_inferences\": self.inference_count,\n        }\n\n    def inference(self, model_name, input_data):\n        \"\"\"Run inference on a loaded model.\"\"\"\n        if model_name not in self.models:\n            raise ValueError(f\"Model not loaded: {model_name}\")\n\n        model = self.models[model_name]\n        self.inference_count += 1\n\n        # Simulate inference (compute a hash as a \"prediction\")\n        time.sleep(0.1)  # Simulate computation time\n\n        result = {\n            \"model\": model_name,\n            \"input_hash\": hashlib.md5(str(input_data).encode()).hexdigest()[:8],\n            \"prediction\": f\"result_{self.inference_count}\",\n            \"confidence\": 0.95,\n            \"inference_time_ms\": 100,\n        }\n        return result\n\n    def unload_model(self, name):\n        \"\"\"Unload a model from memory.\"\"\"\n        if name not in self.models:\n            return {\"status\": \"not_found\", \"name\": name}\n\n        del self.models[name]\n        return {\"status\": \"unloaded\", \"name\": name}\n\n    def close(self):\n        \"\"\"Cleanup on shutdown.\"\"\"\n        print(f\"[MLApp] Shutting down. Total inferences: {self.inference_count}\")\n        self.models.clear()\n\n\nclass ComputeApp(DirtyApp):\n    \"\"\"\n    Example dirty application for CPU-intensive computations.\n\n    This demonstrates operations that would block HTTP workers\n    but are fine in dirty workers.\n    \"\"\"\n\n    def __init__(self):\n        self.computation_count = 0\n\n    def init(self):\n        print(f\"[ComputeApp] Initialized (pid: {__import__('os').getpid()})\")\n\n    def __call__(self, action, *args, **kwargs):\n        method = getattr(self, action, None)\n        if method is None or action.startswith('_'):\n            raise ValueError(f\"Unknown action: {action}\")\n        return method(*args, **kwargs)\n\n    def fibonacci(self, n):\n        \"\"\"Compute fibonacci number (CPU-intensive for large n).\"\"\"\n        self.computation_count += 1\n\n        if n <= 1:\n            return {\"n\": n, \"result\": n, \"computation_id\": self.computation_count}\n\n        a, b = 0, 1\n        for _ in range(2, n + 1):\n            a, b = b, a + b\n\n        return {\"n\": n, \"result\": b, \"computation_id\": self.computation_count}\n\n    def prime_check(self, n):\n        \"\"\"Check if a number is prime (CPU-intensive for large n).\"\"\"\n        self.computation_count += 1\n\n        if n < 2:\n            is_prime = False\n        elif n == 2:\n            is_prime = True\n        elif n % 2 == 0:\n            is_prime = False\n        else:\n            is_prime = True\n            for i in range(3, int(n**0.5) + 1, 2):\n                if n % i == 0:\n                    is_prime = False\n                    break\n\n        return {\"n\": n, \"is_prime\": is_prime, \"computation_id\": self.computation_count}\n\n    def stats(self):\n        \"\"\"Get computation statistics.\"\"\"\n        return {\"total_computations\": self.computation_count}\n\n    def close(self):\n        print(f\"[ComputeApp] Shutting down. Total computations: {self.computation_count}\")\n\n\nclass SessionApp(DirtyApp):\n    \"\"\"\n    Example dirty application demonstrating stash (shared state).\n\n    This shows how multiple dirty workers can share state through\n    the arbiter's stash tables. All workers see the same data.\n    \"\"\"\n\n    # Declare stash tables used by this app (auto-created on startup)\n    stashes = [\"sessions\", \"counters\"]\n\n    def __init__(self):\n        self.worker_pid = None\n\n    def init(self):\n        self.worker_pid = os.getpid()\n        print(f\"[SessionApp] Initialized on worker {self.worker_pid}\")\n        # Initialize a global counter if it doesn't exist\n        if not stash.exists(\"counters\", \"requests\"):\n            stash.put(\"counters\", \"requests\", 0)\n\n    def __call__(self, action, *args, **kwargs):\n        method = getattr(self, action, None)\n        if method is None or action.startswith('_'):\n            raise ValueError(f\"Unknown action: {action}\")\n        return method(*args, **kwargs)\n\n    def login(self, user_id, user_data):\n        \"\"\"Store user session in shared stash.\"\"\"\n        session = {\n            \"user_id\": user_id,\n            \"data\": user_data,\n            \"logged_in_at\": time.time(),\n            \"worker_pid\": self.worker_pid,\n        }\n        stash.put(\"sessions\", f\"user:{user_id}\", session)\n        self._increment_counter()\n        return {\"status\": \"ok\", \"session\": session}\n\n    def logout(self, user_id):\n        \"\"\"Remove user session.\"\"\"\n        key = f\"user:{user_id}\"\n        if stash.exists(\"sessions\", key):\n            stash.delete(\"sessions\", key)\n            self._increment_counter()\n            return {\"status\": \"logged_out\", \"user_id\": user_id}\n        return {\"status\": \"not_found\", \"user_id\": user_id}\n\n    def get_session(self, user_id):\n        \"\"\"Get user session - visible from any worker.\"\"\"\n        session = stash.get(\"sessions\", f\"user:{user_id}\")\n        self._increment_counter()\n        return {\n            \"session\": session,\n            \"served_by_worker\": self.worker_pid,\n        }\n\n    def list_sessions(self):\n        \"\"\"List all active sessions.\"\"\"\n        keys = stash.keys(\"sessions\", pattern=\"user:*\")\n        sessions = []\n        for key in keys:\n            sessions.append(stash.get(\"sessions\", key))\n        self._increment_counter()\n        return {\n            \"sessions\": sessions,\n            \"count\": len(sessions),\n            \"served_by_worker\": self.worker_pid,\n        }\n\n    def get_stats(self):\n        \"\"\"Get global request counter (shared across all workers).\"\"\"\n        count = stash.get(\"counters\", \"requests\", 0)\n        return {\n            \"total_requests\": count,\n            \"served_by_worker\": self.worker_pid,\n        }\n\n    def _increment_counter(self):\n        \"\"\"Increment global request counter.\"\"\"\n        current = stash.get(\"counters\", \"requests\", 0)\n        stash.put(\"counters\", \"requests\", current + 1)\n\n    def clear_all(self):\n        \"\"\"Clear all sessions (for testing).\"\"\"\n        stash.clear(\"sessions\")\n        stash.put(\"counters\", \"requests\", 0)\n        return {\"status\": \"cleared\"}\n\n    def close(self):\n        print(f\"[SessionApp] Shutting down worker {self.worker_pid}\")\n"
  },
  {
    "path": "examples/dirty_example/docker-compose.yml",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nservices:\n  # Run the example tests (protocol, dirty app, worker integration)\n  tests:\n    build:\n      context: ../..\n      dockerfile: examples/dirty_example/Dockerfile\n    command: >\n      bash -c \"\n        echo '=== Running Protocol Tests ===' &&\n        python examples/dirty_example/test_protocol.py &&\n        echo '' &&\n        echo '=== Running Dirty App Tests ===' &&\n        python examples/dirty_example/test_dirty_app.py &&\n        echo '' &&\n        echo '=== Running Worker Integration Tests ===' &&\n        python examples/dirty_example/test_worker_integration.py &&\n        echo '' &&\n        echo '=== All tests passed! ==='\n      \"\n\n  # Run the full gunicorn server with dirty workers\n  server:\n    build:\n      context: ../..\n      dockerfile: examples/dirty_example/Dockerfile\n    ports:\n      - \"8001:8000\"\n    environment:\n      - GUNICORN_BIND=0.0.0.0:8000\n    command: >\n      gunicorn examples.dirty_example.wsgi_app:app\n      -c examples/dirty_example/gunicorn_conf.py\n    healthcheck:\n      test: [\"CMD\", \"python\", \"-c\", \"import urllib.request; urllib.request.urlopen('http://localhost:8000/')\"]\n      interval: 5s\n      timeout: 5s\n      retries: 5\n      start_period: 10s\n\n  # Run integration test against the server\n  integration-test:\n    build:\n      context: ../..\n      dockerfile: examples/dirty_example/Dockerfile\n    depends_on:\n      server:\n        condition: service_healthy\n    environment:\n      - TEST_BASE_URL=http://server:8000\n    command: python examples/dirty_example/test_integration.py\n\n  # Run stash integration test against the server\n  stash-test:\n    build:\n      context: ../..\n      dockerfile: examples/dirty_example/Dockerfile\n    depends_on:\n      server:\n        condition: service_healthy\n    environment:\n      - TEST_BASE_URL=http://server:8000\n    command: python examples/dirty_example/test_stash_integration.py\n"
  },
  {
    "path": "examples/dirty_example/gunicorn_conf.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nGunicorn configuration for Dirty Workers Example\n\nRun with:\n    cd examples/dirty_example\n    gunicorn wsgi_app:app -c gunicorn_conf.py\n\"\"\"\n\n# Basic settings\n# Use 0.0.0.0 for Docker, override with GUNICORN_BIND env var if needed\nimport os\nbind = os.environ.get(\"GUNICORN_BIND\", \"127.0.0.1:8000\")\nworkers = 2\nworker_class = \"sync\"\ntimeout = 30\n\n# Dirty arbiter settings\ndirty_apps = [\n    \"examples.dirty_example.dirty_app:MLApp\",\n    \"examples.dirty_example.dirty_app:ComputeApp\",\n    \"examples.dirty_example.dirty_app:SessionApp\",\n]\ndirty_workers = 2\ndirty_timeout = 300\ndirty_graceful_timeout = 30\n\n# Logging\nloglevel = \"info\"\naccesslog = \"-\"\nerrorlog = \"-\"\n\n\n# Hooks for demonstration\ndef on_starting(server):\n    print(\"=== Gunicorn starting ===\")\n\n\ndef when_ready(server):\n    print(\"=== Gunicorn ready ===\")\n    print(f\"HTTP workers: {server.num_workers}\")\n    print(f\"Dirty workers: {server.cfg.dirty_workers}\")\n    print(f\"Dirty apps: {server.cfg.dirty_apps}\")\n\n\ndef on_dirty_starting(arbiter):\n    print(\"=== Dirty arbiter starting ===\")\n\n\ndef dirty_post_fork(arbiter, worker):\n    print(f\"=== Dirty worker {worker.pid} forked ===\")\n\n\ndef dirty_worker_init(worker):\n    print(f\"=== Dirty worker {worker.pid} initialized apps ===\")\n\n\ndef dirty_worker_exit(arbiter, worker):\n    print(f\"=== Dirty worker {worker.pid} exiting ===\")\n"
  },
  {
    "path": "examples/dirty_example/test_dirty_app.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n#!/usr/bin/env python\n\"\"\"\nTest script to demonstrate Dirty App functionality directly.\n\nThis tests the dirty app without running the full gunicorn server.\n\nRun with:\n    python examples/dirty_example/test_dirty_app.py\n\"\"\"\n\nimport sys\nimport os\n\n# Add parent directory to path\nsys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))\n\nfrom examples.dirty_example.dirty_app import MLApp, ComputeApp\n\n\ndef test_ml_app():\n    \"\"\"Test the MLApp dirty application.\"\"\"\n    print(\"=\" * 60)\n    print(\"Testing MLApp\")\n    print(\"=\" * 60)\n\n    # Create and initialize the app\n    app = MLApp()\n    print(\"\\n1. Initializing app (loads default model)...\")\n    app.init()\n\n    # List models\n    print(\"\\n2. Listing models...\")\n    result = app(\"list_models\")\n    print(f\"   Models: {result}\")\n\n    # Load another model\n    print(\"\\n3. Loading 'gpt-4' model...\")\n    result = app(\"load_model\", \"gpt-4\")\n    print(f\"   Result: {result}\")\n\n    # List models again\n    print(\"\\n4. Listing models again...\")\n    result = app(\"list_models\")\n    print(f\"   Models: {result}\")\n\n    # Run inference\n    print(\"\\n5. Running inference on 'default' model...\")\n    result = app(\"inference\", \"default\", \"Hello, world!\")\n    print(f\"   Result: {result}\")\n\n    # Run more inferences\n    print(\"\\n6. Running more inferences...\")\n    for i in range(3):\n        result = app(\"inference\", \"gpt-4\", f\"Input data {i}\")\n        print(f\"   Inference {i+1}: {result['prediction']}\")\n\n    # Unload a model\n    print(\"\\n7. Unloading 'gpt-4' model...\")\n    result = app(\"unload_model\", \"gpt-4\")\n    print(f\"   Result: {result}\")\n\n    # Final stats\n    print(\"\\n8. Final stats...\")\n    result = app(\"list_models\")\n    print(f\"   {result}\")\n\n    # Close\n    print(\"\\n9. Closing app...\")\n    app.close()\n\n    print(\"\\n\" + \"=\" * 60)\n    print(\"MLApp test complete!\")\n    print(\"=\" * 60)\n\n\ndef test_compute_app():\n    \"\"\"Test the ComputeApp dirty application.\"\"\"\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Testing ComputeApp\")\n    print(\"=\" * 60)\n\n    # Create and initialize\n    app = ComputeApp()\n    app.init()\n\n    # Fibonacci\n    print(\"\\n1. Computing Fibonacci numbers...\")\n    for n in [10, 20, 30, 40]:\n        result = app(\"fibonacci\", n)\n        print(f\"   fib({n}) = {result['result']}\")\n\n    # Prime checks\n    print(\"\\n2. Checking prime numbers...\")\n    for n in [17, 100, 997, 1000]:\n        result = app(\"prime_check\", n)\n        status = \"is prime\" if result['is_prime'] else \"is NOT prime\"\n        print(f\"   {n} {status}\")\n\n    # Stats\n    print(\"\\n3. Stats...\")\n    result = app(\"stats\")\n    print(f\"   {result}\")\n\n    # Close\n    app.close()\n\n    print(\"\\n\" + \"=\" * 60)\n    print(\"ComputeApp test complete!\")\n    print(\"=\" * 60)\n\n\ndef test_error_handling():\n    \"\"\"Test error handling in dirty apps.\"\"\"\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Testing Error Handling\")\n    print(\"=\" * 60)\n\n    app = MLApp()\n    app.init()\n\n    # Try to run inference on non-existent model\n    print(\"\\n1. Trying inference on non-existent model...\")\n    try:\n        app(\"inference\", \"nonexistent\", \"data\")\n    except ValueError as e:\n        print(f\"   Caught expected error: {e}\")\n\n    # Try unknown action\n    print(\"\\n2. Trying unknown action...\")\n    try:\n        app(\"unknown_action\")\n    except ValueError as e:\n        print(f\"   Caught expected error: {e}\")\n\n    # Try private method\n    print(\"\\n3. Trying private method...\")\n    try:\n        app(\"_load_model\", \"test\")\n    except ValueError as e:\n        print(f\"   Caught expected error: {e}\")\n\n    app.close()\n\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Error handling test complete!\")\n    print(\"=\" * 60)\n\n\nif __name__ == \"__main__\":\n    print(\"\\n\" + \"#\" * 60)\n    print(\"# Dirty App Demonstration\")\n    print(\"#\" * 60)\n\n    test_ml_app()\n    test_compute_app()\n    test_error_handling()\n\n    print(\"\\n\" + \"#\" * 60)\n    print(\"# All tests passed!\")\n    print(\"#\" * 60 + \"\\n\")\n"
  },
  {
    "path": "examples/dirty_example/test_integration.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n#!/usr/bin/env python\n\"\"\"\nIntegration test for the dirty example server.\n\nThis tests that the full gunicorn server with dirty workers responds\ncorrectly to HTTP requests.\n\nRun with:\n    python examples/dirty_example/test_integration.py [base_url]\n\nDefault base_url is http://localhost:8000\n\"\"\"\n\nimport sys\nimport os\nimport json\nimport urllib.request\nimport urllib.error\n\n\ndef test_endpoint(base, path, expected_key=None):\n    \"\"\"Test an endpoint and check for expected key in response.\"\"\"\n    url = base + path\n    print(f\"Testing: {url}\")\n    try:\n        with urllib.request.urlopen(url, timeout=10) as resp:\n            data = json.loads(resp.read())\n            print(f\"  Response: {str(data)[:200]}\")\n            if expected_key and expected_key not in data:\n                print(f\"  ERROR: Expected key '{expected_key}' not found!\")\n                return False\n            return True\n    except urllib.error.HTTPError as e:\n        print(f\"  HTTP ERROR {e.code}: {e.reason}\")\n        return False\n    except Exception as e:\n        print(f\"  ERROR: {e}\")\n        return False\n\n\ndef main():\n    # Get base URL from env or command line\n    base = os.environ.get(\"TEST_BASE_URL\", \"http://localhost:8000\")\n    if len(sys.argv) > 1:\n        base = sys.argv[1]\n\n    print(f\"Testing dirty example server at: {base}\")\n    print(\"=\" * 60)\n\n    # Define tests: (path, expected_key_in_response)\n    tests = [\n        (\"/\", \"endpoints\"),\n        (\"/models\", \"models\"),\n        (\"/load?name=test-model\", \"status\"),\n        (\"/inference?model=default&data=hello\", \"prediction\"),\n        (\"/fibonacci?n=20\", \"result\"),\n        (\"/prime?n=17\", \"is_prime\"),\n        (\"/stats\", \"ml_app\"),\n        (\"/unload?name=test-model\", \"status\"),\n    ]\n\n    failed = 0\n    for path, key in tests:\n        if not test_endpoint(base, path, key):\n            failed += 1\n        print()\n\n    print(\"=\" * 60)\n    if failed:\n        print(f\"FAILED: {failed} tests failed\")\n        sys.exit(1)\n    else:\n        print(\"SUCCESS: All integration tests passed!\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/dirty_example/test_protocol.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n#!/usr/bin/env python\n\"\"\"\nTest script to demonstrate the Dirty Binary Protocol layer.\n\nThe binary protocol uses a 16-byte header + TLV-encoded payloads for efficient\nbinary data transfer without base64 encoding overhead.\n\nRun with:\n    python examples/dirty_example/test_protocol.py\n\"\"\"\n\nimport sys\nimport os\nimport asyncio\nimport socket\n\nsys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))\n\nfrom gunicorn.dirty.protocol import (\n    BinaryProtocol,\n    DirtyProtocol,\n    make_request,\n    make_response,\n    make_error_response,\n    HEADER_SIZE,\n    MAGIC,\n    VERSION,\n)\nfrom gunicorn.dirty.errors import DirtyError, DirtyTimeoutError\n\n\ndef test_protocol_encode_decode():\n    \"\"\"Test protocol encoding and decoding.\"\"\"\n    print(\"=\" * 60)\n    print(\"Testing Binary Protocol Encode/Decode\")\n    print(\"=\" * 60)\n\n    # Test request with integer ID (recommended for binary protocol)\n    print(\"\\n1. Creating a request message...\")\n    request = make_request(\n        request_id=12345,  # Integer IDs are efficient\n        app_path=\"myapp.ml:MLApp\",\n        action=\"inference\",\n        args=(\"model1\",),\n        kwargs={\"temperature\": 0.7}\n    )\n    print(f\"   Request: {request}\")\n\n    # Encode using binary protocol\n    print(\"\\n2. Encoding message with binary protocol...\")\n    encoded = BinaryProtocol._encode_from_dict(request)\n    print(f\"   Encoded length: {len(encoded)} bytes\")\n    print(f\"   Header ({HEADER_SIZE} bytes): {encoded[:HEADER_SIZE].hex()}\")\n    print(f\"   Magic: {MAGIC!r}\")\n    print(f\"   Version: {VERSION}\")\n\n    # Decode header\n    print(\"\\n3. Decoding header...\")\n    msg_type, request_id, payload_len = BinaryProtocol.decode_header(encoded[:HEADER_SIZE])\n    print(f\"   Message type: {msg_type} (0x{msg_type:02x})\")\n    print(f\"   Request ID: {request_id}\")\n    print(f\"   Payload length: {payload_len} bytes\")\n\n    # Decode full message\n    print(\"\\n4. Decoding full message...\")\n    msg_type_str, req_id, payload = BinaryProtocol.decode_message(encoded)\n    print(f\"   Type: {msg_type_str}\")\n    print(f\"   Request ID: {req_id}\")\n    print(f\"   Payload: {payload}\")\n\n\ndef test_binary_data_handling():\n    \"\"\"Test binary data handling - the main advantage of binary protocol.\"\"\"\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Testing Binary Data Handling\")\n    print(\"=\" * 60)\n\n    # Create binary data (e.g., image, audio, model weights)\n    binary_data = bytes(range(256))  # All byte values\n    print(f\"\\n1. Original binary data: {len(binary_data)} bytes\")\n    print(f\"   First 16 bytes: {binary_data[:16].hex()}\")\n\n    # Create response with binary data (no base64 needed!)\n    print(\"\\n2. Encoding binary data in response...\")\n    response = make_response(67890, {\"image_data\": binary_data, \"format\": \"raw\"})\n    encoded = BinaryProtocol._encode_from_dict(response)\n    print(f\"   Encoded total size: {len(encoded)} bytes\")\n\n    # Decode and verify\n    print(\"\\n3. Decoding binary data...\")\n    msg_type_str, req_id, payload = BinaryProtocol.decode_message(encoded)\n    recovered_data = payload[\"result\"][\"image_data\"]\n    print(f\"   Recovered data size: {len(recovered_data)} bytes\")\n    print(f\"   Data matches: {recovered_data == binary_data}\")\n    print(f\"   First 16 bytes: {recovered_data[:16].hex()}\")\n\n\ndef test_protocol_response():\n    \"\"\"Test response message building.\"\"\"\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Testing Response Messages\")\n    print(\"=\" * 60)\n\n    # Success response\n    print(\"\\n1. Creating success response...\")\n    response = make_response(12345, {\"result\": \"Hello, World!\", \"confidence\": 0.95})\n    print(f\"   Response: {response}\")\n\n    # Error response\n    print(\"\\n2. Creating error response...\")\n    error = DirtyTimeoutError(\"Operation timed out\", timeout=30)\n    error_response = make_error_response(12345, error)\n    print(f\"   Error response: {error_response}\")\n\n\ndef test_socket_communication():\n    \"\"\"Test sync protocol over actual sockets.\"\"\"\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Testing Socket Communication\")\n    print(\"=\" * 60)\n\n    # Create a socket pair\n    server_sock, client_sock = socket.socketpair()\n\n    try:\n        # Send a request\n        print(\"\\n1. Sending request over socket...\")\n        request = make_request(\n            request_id=100001,\n            app_path=\"test:App\",\n            action=\"compute\",\n            args=(1, 2, 3),\n            kwargs={}\n        )\n        DirtyProtocol.write_message(client_sock, request)\n        print(f\"   Sent: {request}\")\n\n        # Receive the request\n        print(\"\\n2. Receiving request...\")\n        received = DirtyProtocol.read_message(server_sock)\n        print(f\"   Received: {received}\")\n        print(f\"   Request ID: {received['id']}\")\n\n        # Send a response with binary data\n        print(\"\\n3. Sending response with binary data...\")\n        binary_result = b\"\\x00\\x01\\x02\\x03\\xff\\xfe\\xfd\\xfc\"\n        response = make_response(100001, {\"data\": binary_result, \"sum\": 6})\n        DirtyProtocol.write_message(server_sock, response)\n        print(f\"   Sent binary data: {binary_result.hex()}\")\n\n        # Receive the response\n        print(\"\\n4. Receiving response...\")\n        received = DirtyProtocol.read_message(client_sock)\n        print(f\"   Received binary data: {received['result']['data'].hex()}\")\n        print(f\"   Sum: {received['result']['sum']}\")\n\n    finally:\n        server_sock.close()\n        client_sock.close()\n\n\nasync def test_async_communication():\n    \"\"\"Test async protocol over streams.\"\"\"\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Testing Async Communication\")\n    print(\"=\" * 60)\n\n    # Use a pipe for async testing\n    read_fd, write_fd = os.pipe()\n\n    try:\n        # Create message\n        request = make_request(\n            request_id=200001,\n            app_path=\"async:App\",\n            action=\"process\",\n            args=(\"data\",),\n            kwargs={\"async\": True}\n        )\n\n        # Write to pipe\n        print(\"\\n1. Writing async message...\")\n        encoded = BinaryProtocol._encode_from_dict(request)\n        os.write(write_fd, encoded)\n        os.close(write_fd)\n        write_fd = None\n        print(f\"   Wrote {len(encoded)} bytes\")\n\n        # Read from pipe using async reader\n        print(\"\\n2. Reading async message...\")\n        reader = asyncio.StreamReader()\n        data = os.read(read_fd, len(encoded))\n        reader.feed_data(data)\n        reader.feed_eof()\n\n        received = await DirtyProtocol.read_message_async(reader)\n        print(f\"   Received: {received}\")\n        print(f\"   Request ID: {received['id']}\")\n\n    finally:\n        if write_fd is not None:\n            os.close(write_fd)\n        os.close(read_fd)\n\n\ndef test_error_serialization():\n    \"\"\"Test error serialization and deserialization.\"\"\"\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Testing Error Serialization\")\n    print(\"=\" * 60)\n\n    # Create various errors\n    errors = [\n        DirtyError(\"Generic error\", {\"code\": 500}),\n        DirtyTimeoutError(\"Timeout!\", timeout=60),\n    ]\n\n    for error in errors:\n        print(f\"\\n1. Original error: {error}\")\n        print(f\"   Type: {type(error).__name__}\")\n\n        # Serialize\n        error_dict = error.to_dict()\n        print(f\"2. Serialized: {error_dict}\")\n\n        # Deserialize\n        restored = DirtyError.from_dict(error_dict)\n        print(f\"3. Restored: {restored}\")\n        print(f\"   Type: {type(restored).__name__}\")\n        print(f\"   Match type: {type(restored) == type(error)}\")\n\n\nif __name__ == \"__main__\":\n    print(\"\\n\" + \"#\" * 60)\n    print(\"# Dirty Binary Protocol Demonstration\")\n    print(\"#\" * 60)\n\n    test_protocol_encode_decode()\n    test_binary_data_handling()\n    test_protocol_response()\n    test_socket_communication()\n    asyncio.run(test_async_communication())\n    test_error_serialization()\n\n    print(\"\\n\" + \"#\" * 60)\n    print(\"# All protocol tests passed!\")\n    print(\"#\" * 60 + \"\\n\")\n"
  },
  {
    "path": "examples/dirty_example/test_stash_integration.py",
    "content": "#!/usr/bin/env python3\n#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nIntegration tests for stash (shared state) functionality.\n\nThese tests verify that stash works correctly across multiple dirty workers,\ndemonstrating that state is truly shared.\n\nRun with Docker:\n    docker-compose up --build\n    docker-compose exec app python test_stash_integration.py\n\"\"\"\n\nimport json\nimport os\nimport sys\nimport urllib.request\nimport urllib.error\n\nBASE_URL = os.environ.get(\"TEST_BASE_URL\", \"http://localhost:8000\")\n\n\ndef request(path):\n    \"\"\"Make HTTP request and return JSON response.\"\"\"\n    url = f\"{BASE_URL}{path}\"\n    try:\n        with urllib.request.urlopen(url, timeout=10) as resp:\n            return json.loads(resp.read().decode())\n    except urllib.error.HTTPError as e:\n        return {\"error\": str(e), \"code\": e.code}\n    except urllib.error.URLError as e:\n        return {\"error\": str(e)}\n\n\ndef test_stash_shared_state():\n    \"\"\"Test that stash state is shared across workers.\"\"\"\n    print(\"\\n=== Test: Stash Shared State ===\")\n\n    # Clear any existing state\n    result = request(\"/session/clear\")\n    print(f\"Clear: {result}\")\n\n    # Login a user\n    result = request(\"/session/login?user_id=100&name=Alice\")\n    print(f\"Login Alice: {result}\")\n    assert result.get(\"status\") == \"ok\", f\"Login failed: {result}\"\n    worker1 = result.get(\"session\", {}).get(\"worker_pid\")\n    print(f\"  -> Handled by worker: {worker1}\")\n\n    # Make multiple requests to potentially hit different workers\n    # and verify they all see the same session\n    workers_seen = set()\n    for i in range(5):\n        result = request(\"/session/get?user_id=100\")\n        worker = result.get(\"served_by_worker\")\n        workers_seen.add(worker)\n        session = result.get(\"session\")\n        assert session is not None, f\"Session not found on request {i+1}\"\n        assert session.get(\"data\", {}).get(\"name\") == \"Alice\", f\"Wrong session data\"\n\n    print(f\"  -> Session visible from workers: {workers_seen}\")\n    print(\"PASSED: State is shared across workers\")\n    return True\n\n\ndef test_stash_counter():\n    \"\"\"Test that global counter increments correctly.\"\"\"\n    print(\"\\n=== Test: Global Counter ===\")\n\n    # Clear state\n    request(\"/session/clear\")\n\n    # Get initial stats\n    result = request(\"/session/stats\")\n    initial = result.get(\"total_requests\", 0)\n    print(f\"Initial count: {initial}\")\n\n    # Make several requests\n    for i in range(5):\n        request(f\"/session/login?user_id={i}&name=User{i}\")\n\n    # Check counter increased\n    result = request(\"/session/stats\")\n    final = result.get(\"total_requests\", 0)\n    print(f\"Final count: {final}\")\n\n    # Each login increments counter by 1\n    assert final >= initial + 5, f\"Counter didn't increment enough: {initial} -> {final}\"\n    print(\"PASSED: Global counter works across workers\")\n    return True\n\n\ndef test_stash_list_sessions():\n    \"\"\"Test listing all sessions.\"\"\"\n    print(\"\\n=== Test: List Sessions ===\")\n\n    # Clear and create some sessions\n    request(\"/session/clear\")\n    request(\"/session/login?user_id=1&name=Alice\")\n    request(\"/session/login?user_id=2&name=Bob\")\n    request(\"/session/login?user_id=3&name=Charlie\")\n\n    # List all sessions\n    result = request(\"/session/list\")\n    sessions = result.get(\"sessions\", [])\n    count = result.get(\"count\", 0)\n\n    print(f\"Sessions: {count}\")\n    for s in sessions:\n        print(f\"  - user:{s.get('user_id')} = {s.get('data', {}).get('name')}\")\n\n    assert count == 3, f\"Expected 3 sessions, got {count}\"\n    print(\"PASSED: List sessions works\")\n    return True\n\n\ndef test_stash_logout():\n    \"\"\"Test session deletion.\"\"\"\n    print(\"\\n=== Test: Logout (Delete) ===\")\n\n    # Clear and create a session\n    request(\"/session/clear\")\n    request(\"/session/login?user_id=999&name=TestUser\")\n\n    # Verify it exists\n    result = request(\"/session/get?user_id=999\")\n    assert result.get(\"session\") is not None, \"Session should exist\"\n\n    # Logout\n    result = request(\"/session/logout?user_id=999\")\n    print(f\"Logout: {result}\")\n    assert result.get(\"status\") == \"logged_out\", f\"Logout failed: {result}\"\n\n    # Verify it's gone\n    result = request(\"/session/get?user_id=999\")\n    assert result.get(\"session\") is None, \"Session should be deleted\"\n\n    print(\"PASSED: Logout deletes session\")\n    return True\n\n\ndef test_multiple_workers_see_updates():\n    \"\"\"Test that updates from one worker are visible to others.\"\"\"\n    print(\"\\n=== Test: Cross-Worker Updates ===\")\n\n    request(\"/session/clear\")\n\n    # Create sessions and track which workers handled them\n    workers = {}\n    for i in range(10):\n        result = request(f\"/session/login?user_id={i}&name=User{i}\")\n        worker = result.get(\"session\", {}).get(\"worker_pid\")\n        workers[i] = worker\n\n    unique_workers = set(workers.values())\n    print(f\"Sessions created by workers: {unique_workers}\")\n\n    # Now read all sessions and verify all workers can see all data\n    result = request(\"/session/list\")\n    count = result.get(\"count\", 0)\n    served_by = result.get(\"served_by_worker\")\n\n    print(f\"List returned {count} sessions, served by worker {served_by}\")\n    assert count == 10, f\"Expected 10 sessions, got {count}\"\n\n    print(\"PASSED: All workers see all updates\")\n    return True\n\n\ndef main():\n    \"\"\"Run all tests.\"\"\"\n    print(\"=\" * 60)\n    print(\"Stash Integration Tests\")\n    print(\"=\" * 60)\n\n    # Check server is running\n    try:\n        result = request(\"/\")\n        if \"error\" in result and \"Connection refused\" in str(result.get(\"error\", \"\")):\n            print(\"ERROR: Server not running. Start with: docker-compose up\")\n            return 1\n        if not result.get(\"dirty_enabled\"):\n            print(\"ERROR: Dirty workers not enabled\")\n            return 1\n        print(f\"Server running, dirty workers enabled\")\n    except Exception as e:\n        print(f\"ERROR: Cannot connect to server: {e}\")\n        return 1\n\n    # Run tests\n    tests = [\n        test_stash_shared_state,\n        test_stash_counter,\n        test_stash_list_sessions,\n        test_stash_logout,\n        test_multiple_workers_see_updates,\n    ]\n\n    passed = 0\n    failed = 0\n\n    for test in tests:\n        try:\n            if test():\n                passed += 1\n            else:\n                failed += 1\n        except AssertionError as e:\n            print(f\"FAILED: {e}\")\n            failed += 1\n        except Exception as e:\n            print(f\"ERROR: {e}\")\n            failed += 1\n\n    print(\"\\n\" + \"=\" * 60)\n    print(f\"Results: {passed} passed, {failed} failed\")\n    print(\"=\" * 60)\n\n    return 0 if failed == 0 else 1\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n"
  },
  {
    "path": "examples/dirty_example/test_worker_integration.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n#!/usr/bin/env python\n\"\"\"\nIntegration test demonstrating DirtyWorker execution.\n\nThis test demonstrates how the DirtyWorker loads apps and handles requests\nwithout actually forking processes (suitable for a quick test).\n\nRun with:\n    python examples/dirty_example/test_worker_integration.py\n\"\"\"\n\nimport sys\nimport os\nimport asyncio\nimport tempfile\n\nsys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))\n\nfrom gunicorn.config import Config\nfrom gunicorn.dirty.worker import DirtyWorker\nfrom gunicorn.dirty.protocol import DirtyProtocol, BinaryProtocol, make_request, HEADER_SIZE\n\n\nclass MockLog:\n    \"\"\"Mock logger for testing.\"\"\"\n    def debug(self, msg, *args): print(f\"[DEBUG] {msg % args if args else msg}\")\n    def info(self, msg, *args): print(f\"[INFO] {msg % args if args else msg}\")\n    def warning(self, msg, *args): print(f\"[WARN] {msg % args if args else msg}\")\n    def error(self, msg, *args): print(f\"[ERROR] {msg % args if args else msg}\")\n    def close_on_exec(self): pass\n    def reopen_files(self): pass\n\n\nclass MockWriter:\n    \"\"\"Mock StreamWriter that captures written responses.\"\"\"\n\n    def __init__(self):\n        self.messages = []\n        self._buffer = b\"\"\n\n    def write(self, data):\n        self._buffer += data\n\n    async def drain(self):\n        # Decode messages from buffer using binary protocol\n        while len(self._buffer) >= HEADER_SIZE:\n            _, _, length = BinaryProtocol.decode_header(self._buffer[:HEADER_SIZE])\n            total_size = HEADER_SIZE + length\n            if len(self._buffer) >= total_size:\n                msg_data = self._buffer[:total_size]\n                self._buffer = self._buffer[total_size:]\n                msg_type_str, request_id, payload_dict = BinaryProtocol.decode_message(msg_data)\n                result = {\"type\": msg_type_str, \"id\": request_id}\n                result.update(payload_dict)\n                self.messages.append(result)\n            else:\n                break\n\n    def get_last_response(self):\n        \"\"\"Get the last response message.\"\"\"\n        return self.messages[-1] if self.messages else None\n\n\nasync def test_worker_request_handling():\n    \"\"\"Test that a worker can load apps and handle requests.\"\"\"\n    print(\"=\" * 60)\n    print(\"Testing DirtyWorker Request Handling\")\n    print(\"=\" * 60)\n\n    # Create config and worker\n    cfg = Config()\n    log = MockLog()\n\n    with tempfile.TemporaryDirectory() as tmpdir:\n        socket_path = os.path.join(tmpdir, \"worker.sock\")\n\n        worker = DirtyWorker(\n            age=1,\n            ppid=os.getpid(),\n            app_paths=[\"examples.dirty_example.dirty_app:MLApp\"],\n            cfg=cfg,\n            log=log,\n            socket_path=socket_path\n        )\n\n        # Load apps (normally done in init_process after fork)\n        print(\"\\n1. Loading apps...\")\n        worker.load_apps()\n        print(f\"   Loaded apps: {list(worker.apps.keys())}\")\n\n        # Test execute directly\n        print(\"\\n2. Testing execute() - list_models...\")\n        result = await worker.execute(\n            \"examples.dirty_example.dirty_app:MLApp\",\n            \"list_models\",\n            [],\n            {}\n        )\n        print(f\"   Result: {result}\")\n\n        # Test handle_request with a proper request message\n        print(\"\\n3. Testing handle_request() - load_model...\")\n        request = make_request(\n            request_id=1001,\n            app_path=\"examples.dirty_example.dirty_app:MLApp\",\n            action=\"load_model\",\n            args=(\"gpt-4\",),\n            kwargs={}\n        )\n        writer = MockWriter()\n        await worker.handle_request(request, writer)\n        response = writer.get_last_response()\n        print(f\"   Response type: {response['type']}\")\n        print(f\"   Result: {response.get('result', response.get('error'))}\")\n\n        # Test inference\n        print(\"\\n4. Testing handle_request() - inference...\")\n        request = make_request(\n            request_id=1002,\n            app_path=\"examples.dirty_example.dirty_app:MLApp\",\n            action=\"inference\",\n            args=(\"default\", \"Hello AI!\"),\n            kwargs={}\n        )\n        writer = MockWriter()\n        await worker.handle_request(request, writer)\n        response = writer.get_last_response()\n        print(f\"   Response type: {response['type']}\")\n        print(f\"   Result: {response.get('result', response.get('error'))}\")\n\n        # Test error handling\n        print(\"\\n5. Testing error handling - unknown action...\")\n        request = make_request(\n            request_id=1003,\n            app_path=\"examples.dirty_example.dirty_app:MLApp\",\n            action=\"nonexistent_action\",\n            args=(),\n            kwargs={}\n        )\n        writer = MockWriter()\n        await worker.handle_request(request, writer)\n        response = writer.get_last_response()\n        print(f\"   Response type: {response['type']}\")\n        print(f\"   Error: {response.get('error', {}).get('message')}\")\n\n        # Test app not found\n        print(\"\\n6. Testing error handling - app not found...\")\n        request = make_request(\n            request_id=1004,\n            app_path=\"nonexistent:App\",\n            action=\"test\",\n            args=(),\n            kwargs={}\n        )\n        writer = MockWriter()\n        await worker.handle_request(request, writer)\n        response = writer.get_last_response()\n        print(f\"   Response type: {response['type']}\")\n        print(f\"   Error type: {response.get('error', {}).get('error_type')}\")\n\n        # Cleanup\n        print(\"\\n7. Cleanup...\")\n        worker._cleanup()\n        print(\"   Done!\")\n\n\nasync def test_worker_with_compute_app():\n    \"\"\"Test worker with ComputeApp.\"\"\"\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Testing DirtyWorker with ComputeApp\")\n    print(\"=\" * 60)\n\n    cfg = Config()\n    log = MockLog()\n\n    with tempfile.TemporaryDirectory() as tmpdir:\n        socket_path = os.path.join(tmpdir, \"worker.sock\")\n\n        worker = DirtyWorker(\n            age=1,\n            ppid=os.getpid(),\n            app_paths=[\"examples.dirty_example.dirty_app:ComputeApp\"],\n            cfg=cfg,\n            log=log,\n            socket_path=socket_path\n        )\n\n        worker.load_apps()\n\n        # Fibonacci\n        print(\"\\n1. Computing Fibonacci(30)...\")\n        result = await worker.execute(\n            \"examples.dirty_example.dirty_app:ComputeApp\",\n            \"fibonacci\",\n            [30],\n            {}\n        )\n        print(f\"   Result: {result}\")\n\n        # Prime check\n        print(\"\\n2. Checking if 997 is prime...\")\n        result = await worker.execute(\n            \"examples.dirty_example.dirty_app:ComputeApp\",\n            \"prime_check\",\n            [997],\n            {}\n        )\n        print(f\"   Result: {result}\")\n\n        worker._cleanup()\n\n\nasync def test_multiple_apps():\n    \"\"\"Test worker with multiple apps loaded.\"\"\"\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Testing DirtyWorker with Multiple Apps\")\n    print(\"=\" * 60)\n\n    cfg = Config()\n    log = MockLog()\n\n    with tempfile.TemporaryDirectory() as tmpdir:\n        socket_path = os.path.join(tmpdir, \"worker.sock\")\n\n        worker = DirtyWorker(\n            age=1,\n            ppid=os.getpid(),\n            app_paths=[\n                \"examples.dirty_example.dirty_app:MLApp\",\n                \"examples.dirty_example.dirty_app:ComputeApp\",\n            ],\n            cfg=cfg,\n            log=log,\n            socket_path=socket_path\n        )\n\n        worker.load_apps()\n        print(f\"\\n1. Loaded {len(worker.apps)} apps: {list(worker.apps.keys())}\")\n\n        # Use both apps\n        print(\"\\n2. Using MLApp for inference...\")\n        result = await worker.execute(\n            \"examples.dirty_example.dirty_app:MLApp\",\n            \"inference\",\n            [\"default\", \"test input\"],\n            {}\n        )\n        print(f\"   MLApp result: {result['prediction']}\")\n\n        print(\"\\n3. Using ComputeApp for fibonacci...\")\n        result = await worker.execute(\n            \"examples.dirty_example.dirty_app:ComputeApp\",\n            \"fibonacci\",\n            [15],\n            {}\n        )\n        print(f\"   ComputeApp result: fib(15) = {result['result']}\")\n\n        worker._cleanup()\n\n\nif __name__ == \"__main__\":\n    print(\"\\n\" + \"#\" * 60)\n    print(\"# DirtyWorker Integration Demonstration\")\n    print(\"#\" * 60)\n\n    asyncio.run(test_worker_request_handling())\n    asyncio.run(test_worker_with_compute_app())\n    asyncio.run(test_multiple_apps())\n\n    print(\"\\n\" + \"#\" * 60)\n    print(\"# All integration tests passed!\")\n    print(\"#\" * 60 + \"\\n\")\n"
  },
  {
    "path": "examples/dirty_example/wsgi_app.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nExample WSGI Application that uses Dirty Workers\n\nThis demonstrates how HTTP workers can call dirty workers\nfor heavy operations like ML inference.\n\nRun with:\n    cd examples/dirty_example\n    gunicorn wsgi_app:app -c gunicorn_conf.py\n\"\"\"\n\nimport json\nimport os\nfrom urllib.parse import parse_qs\n\n\ndef get_dirty_client():\n    \"\"\"Get the dirty client, with fallback for when dirty workers aren't enabled.\"\"\"\n    try:\n        from gunicorn.dirty import get_dirty_client as _get_dirty_client\n        return _get_dirty_client()\n    except Exception as e:\n        return None\n\n\ndef app(environ, start_response):\n    \"\"\"WSGI application that demonstrates dirty worker integration.\"\"\"\n    path = environ.get('PATH_INFO', '/')\n    method = environ.get('REQUEST_METHOD', 'GET')\n\n    # Parse query string\n    query = parse_qs(environ.get('QUERY_STRING', ''))\n\n    # Get dirty client\n    client = get_dirty_client()\n\n    try:\n        if path == '/':\n            result = {\n                \"message\": \"Dirty Workers Demo\",\n                \"dirty_enabled\": client is not None,\n                \"pid\": os.getpid(),\n                \"endpoints\": {\n                    \"/models\": \"List loaded models\",\n                    \"/load?name=MODEL\": \"Load a model\",\n                    \"/inference?model=NAME&data=INPUT\": \"Run inference\",\n                    \"/unload?name=MODEL\": \"Unload a model\",\n                    \"/fibonacci?n=NUMBER\": \"Compute fibonacci\",\n                    \"/prime?n=NUMBER\": \"Check if prime\",\n                    \"/stats\": \"Get dirty worker stats\",\n                    \"/session/login?user_id=ID&name=NAME\": \"Login user (stash demo)\",\n                    \"/session/get?user_id=ID\": \"Get session (stash demo)\",\n                    \"/session/list\": \"List all sessions (stash demo)\",\n                    \"/session/logout?user_id=ID\": \"Logout user (stash demo)\",\n                    \"/session/stats\": \"Get stash stats (stash demo)\",\n                }\n            }\n\n        elif path == '/models':\n            if client is None:\n                result = {\"error\": \"Dirty workers not enabled\"}\n            else:\n                result = client.execute(\n                    \"examples.dirty_example.dirty_app:MLApp\",\n                    \"list_models\"\n                )\n\n        elif path == '/load':\n            name = query.get('name', ['model1'])[0]\n            if client is None:\n                result = {\"error\": \"Dirty workers not enabled\"}\n            else:\n                result = client.execute(\n                    \"examples.dirty_example.dirty_app:MLApp\",\n                    \"load_model\",\n                    name\n                )\n\n        elif path == '/inference':\n            model = query.get('model', ['default'])[0]\n            data = query.get('data', ['test input'])[0]\n            if client is None:\n                result = {\"error\": \"Dirty workers not enabled\"}\n            else:\n                result = client.execute(\n                    \"examples.dirty_example.dirty_app:MLApp\",\n                    \"inference\",\n                    model,\n                    data\n                )\n\n        elif path == '/unload':\n            name = query.get('name', ['model1'])[0]\n            if client is None:\n                result = {\"error\": \"Dirty workers not enabled\"}\n            else:\n                result = client.execute(\n                    \"examples.dirty_example.dirty_app:MLApp\",\n                    \"unload_model\",\n                    name\n                )\n\n        elif path == '/fibonacci':\n            n = int(query.get('n', ['10'])[0])\n            if client is None:\n                result = {\"error\": \"Dirty workers not enabled\"}\n            else:\n                result = client.execute(\n                    \"examples.dirty_example.dirty_app:ComputeApp\",\n                    \"fibonacci\",\n                    n\n                )\n\n        elif path == '/prime':\n            n = int(query.get('n', ['17'])[0])\n            if client is None:\n                result = {\"error\": \"Dirty workers not enabled\"}\n            else:\n                result = client.execute(\n                    \"examples.dirty_example.dirty_app:ComputeApp\",\n                    \"prime_check\",\n                    n\n                )\n\n        elif path == '/stats':\n            if client is None:\n                result = {\"error\": \"Dirty workers not enabled\"}\n            else:\n                ml_stats = client.execute(\n                    \"examples.dirty_example.dirty_app:MLApp\",\n                    \"list_models\"\n                )\n                compute_stats = client.execute(\n                    \"examples.dirty_example.dirty_app:ComputeApp\",\n                    \"stats\"\n                )\n                result = {\n                    \"ml_app\": ml_stats,\n                    \"compute_app\": compute_stats,\n                    \"http_worker_pid\": os.getpid(),\n                }\n\n        # =====================================================================\n        # Session endpoints (stash demo)\n        # =====================================================================\n        elif path == '/session/login':\n            user_id = query.get('user_id', ['1'])[0]\n            name = query.get('name', ['Anonymous'])[0]\n            if client is None:\n                result = {\"error\": \"Dirty workers not enabled\"}\n            else:\n                result = client.execute(\n                    \"examples.dirty_example.dirty_app:SessionApp\",\n                    \"login\",\n                    user_id=user_id,\n                    user_data={\"name\": name}\n                )\n\n        elif path == '/session/get':\n            user_id = query.get('user_id', ['1'])[0]\n            if client is None:\n                result = {\"error\": \"Dirty workers not enabled\"}\n            else:\n                result = client.execute(\n                    \"examples.dirty_example.dirty_app:SessionApp\",\n                    \"get_session\",\n                    user_id=user_id\n                )\n\n        elif path == '/session/list':\n            if client is None:\n                result = {\"error\": \"Dirty workers not enabled\"}\n            else:\n                result = client.execute(\n                    \"examples.dirty_example.dirty_app:SessionApp\",\n                    \"list_sessions\"\n                )\n\n        elif path == '/session/logout':\n            user_id = query.get('user_id', ['1'])[0]\n            if client is None:\n                result = {\"error\": \"Dirty workers not enabled\"}\n            else:\n                result = client.execute(\n                    \"examples.dirty_example.dirty_app:SessionApp\",\n                    \"logout\",\n                    user_id=user_id\n                )\n\n        elif path == '/session/stats':\n            if client is None:\n                result = {\"error\": \"Dirty workers not enabled\"}\n            else:\n                result = client.execute(\n                    \"examples.dirty_example.dirty_app:SessionApp\",\n                    \"get_stats\"\n                )\n\n        elif path == '/session/clear':\n            if client is None:\n                result = {\"error\": \"Dirty workers not enabled\"}\n            else:\n                result = client.execute(\n                    \"examples.dirty_example.dirty_app:SessionApp\",\n                    \"clear_all\"\n                )\n\n        else:\n            start_response('404 Not Found', [('Content-Type', 'application/json')])\n            return [json.dumps({\"error\": \"Not found\"}).encode()]\n\n        # Success response\n        start_response('200 OK', [('Content-Type', 'application/json')])\n        return [json.dumps(result, indent=2).encode()]\n\n    except Exception as e:\n        start_response('500 Internal Server Error', [('Content-Type', 'application/json')])\n        return [json.dumps({\n            \"error\": str(e),\n            \"type\": type(e).__name__\n        }).encode()]\n"
  },
  {
    "path": "examples/echo.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n#\n# Example code from Eventlet sources\n\nfrom gunicorn import __version__\n\n\ndef app(environ, start_response):\n    \"\"\"Simplest possible application object\"\"\"\n\n    if environ['REQUEST_METHOD'].upper() != 'POST':\n        data = b'Hello, World!\\n'\n    else:\n        data = environ['wsgi.input'].read()\n\n    status = '200 OK'\n\n    response_headers = [\n        ('Content-type', 'text/plain'),\n        ('Content-Length', str(len(data))),\n        ('X-Gunicorn-Version', __version__)\n    ]\n    start_response(status, response_headers)\n    return iter([data])\n"
  },
  {
    "path": "examples/embedding_service/Dockerfile",
    "content": "FROM python:3.12-slim\n\nWORKDIR /app\n\n# Install dependencies\nRUN pip install --no-cache-dir \\\n    sentence-transformers \\\n    fastapi \\\n    pydantic\n\n# Copy gunicorn source\nCOPY . /app/gunicorn-src\nRUN pip install /app/gunicorn-src\n\n# Copy app\nCOPY examples/embedding_service /app/embedding_service\n\nENV PYTHONPATH=/app\n\nEXPOSE 8000\nCMD [\"gunicorn\", \"embedding_service.main:app\", \"-c\", \"embedding_service/gunicorn_conf.py\"]\n"
  },
  {
    "path": "examples/embedding_service/README.md",
    "content": "# Embedding Service Example\n\nA FastAPI-based text embedding service using sentence-transformers, powered by\ngunicorn's dirty workers for efficient ML model management.\n\n## Overview\n\nThis example demonstrates how to build a production-ready embedding API that:\n- Keeps ML models loaded in memory across requests (dirty workers)\n- Handles HTTP efficiently with async FastAPI (ASGI workers)\n- Provides batch embedding for multiple texts\n- Includes Docker-based deployment and testing\n\n## Architecture\n\n```\n┌─────────────────┐     ┌──────────────────┐     ┌─────────────────────┐\n│  HTTP Clients   │────►│  FastAPI (ASGI)  │────►│  DirtyWorker        │\n│                 │     │  - /embed        │     │  - sentence-        │\n│                 │◄────│  - /health       │◄────│    transformers     │\n└─────────────────┘     └──────────────────┘     │  - Model in memory  │\n                                                  └─────────────────────┘\n```\n\n**Why dirty workers?**\n- ML models are expensive to load (several seconds)\n- Dirty workers load the model once at startup\n- HTTP workers remain lightweight and responsive\n- Model stays in memory, serving many requests\n\n## Quick Start\n\n### With Docker (recommended)\n\n```bash\ncd examples/embedding_service\ndocker compose up --build\n```\n\n### Local Development\n\n```bash\n# Install dependencies\npip install sentence-transformers fastapi pydantic\n\n# Run with gunicorn\ngunicorn examples.embedding_service.main:app \\\n  -c examples/embedding_service/gunicorn_conf.py\n```\n\n## API Reference\n\n### POST /embed\n\nGenerate embeddings for a list of texts.\n\n**Request:**\n```json\n{\n  \"texts\": [\"Hello world\", \"Another sentence\"]\n}\n```\n\n**Response:**\n```json\n{\n  \"embeddings\": [\n    [0.123, -0.456, ...],\n    [0.789, -0.012, ...]\n  ]\n}\n```\n\n**Example:**\n```bash\ncurl -X POST http://localhost:8000/embed \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"texts\": [\"Hello world\"]}'\n```\n\n### GET /health\n\nHealth check endpoint.\n\n**Response:**\n```json\n{\"status\": \"ok\"}\n```\n\n## Configuration\n\nEdit `gunicorn_conf.py` to adjust:\n\n| Setting | Default | Description |\n|---------|---------|-------------|\n| `workers` | 2 | Number of HTTP workers |\n| `dirty_workers` | 1 | Number of ML model workers |\n| `dirty_timeout` | 60 | Max seconds per inference |\n| `bind` | 0.0.0.0:8000 | Listen address |\n\n## Model\n\nUses [all-MiniLM-L6-v2](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2):\n- 384-dimensional embeddings\n- Fast inference (~14K sentences/sec on GPU)\n- Good quality for semantic search\n- ~90MB download\n\nTo use a different model, edit `embedding_app.py`:\n```python\nself.model = SentenceTransformer('your-model-name')\n```\n\n## Testing\n\nRun the integration tests:\n\n```bash\n# Start the service first\ndocker compose up -d\n\n# Run tests\npip install requests numpy\npython test_embedding.py\n```\n\n## Production Considerations\n\n1. **GPU Support**: Add CUDA to the Dockerfile for faster inference\n2. **Scaling**: Increase `dirty_workers` for more concurrent embeddings\n3. **Caching**: Add Redis caching for repeated texts\n4. **Rate Limiting**: Add FastAPI middleware for rate limiting\n5. **Monitoring**: Add Prometheus metrics endpoint\n"
  },
  {
    "path": "examples/embedding_service/__init__.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n# Embedding service package\n"
  },
  {
    "path": "examples/embedding_service/docker-compose.yml",
    "content": "services:\n  embedding-service:\n    build:\n      context: ../..\n      dockerfile: examples/embedding_service/Dockerfile\n    ports:\n      - \"8000:8000\"\n    healthcheck:\n      test: [\"CMD\", \"python\", \"-c\", \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=5)\"]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n      start_period: 30s  # Model loading time\n"
  },
  {
    "path": "examples/embedding_service/embedding_app.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.dirty.app import DirtyApp\n\n\nclass EmbeddingApp(DirtyApp):\n    def init(self):\n        from sentence_transformers import SentenceTransformer\n        self.model = SentenceTransformer('all-MiniLM-L6-v2')\n\n    def embed(self, texts):\n        embeddings = self.model.encode(texts)\n        return embeddings.tolist()\n\n    def close(self):\n        del self.model\n"
  },
  {
    "path": "examples/embedding_service/gunicorn_conf.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nbind = \"0.0.0.0:8000\"\nworkers = 2\nworker_class = \"asgi\"\n\n# Dirty worker config\ndirty_apps = [\"embedding_service.embedding_app:EmbeddingApp\"]\ndirty_workers = 1\ndirty_timeout = 60\n"
  },
  {
    "path": "examples/embedding_service/main.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom fastapi import FastAPI\nfrom pydantic import BaseModel\nfrom gunicorn.dirty.client import get_dirty_client\n\napp = FastAPI()\n\n\nclass EmbedRequest(BaseModel):\n    texts: list[str]\n\n\nclass EmbedResponse(BaseModel):\n    embeddings: list[list[float]]\n\n\n@app.post(\"/embed\", response_model=EmbedResponse)\nasync def embed(request: EmbedRequest):\n    client = get_dirty_client()\n    result = client.execute(\n        \"embedding_service.embedding_app:EmbeddingApp\",\n        \"embed\",\n        request.texts\n    )\n    return EmbedResponse(embeddings=result)\n\n\n@app.get(\"/health\")\nasync def health():\n    return {\"status\": \"ok\"}\n"
  },
  {
    "path": "examples/embedding_service/requirements.txt",
    "content": "sentence-transformers\nfastapi\npydantic\nrequests\nnumpy\n"
  },
  {
    "path": "examples/embedding_service/test_embedding.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport os\nimport requests\nimport numpy as np\n\n\ndef test_embedding_endpoint():\n    base_url = os.environ.get(\"EMBEDDING_SERVICE_URL\", \"http://127.0.0.1:8000\")\n    url = f\"{base_url}/embed\"\n\n    # Test single text\n    response = requests.post(url, json={\"texts\": [\"Hello world\"]})\n    assert response.status_code == 200\n    data = response.json()\n    assert len(data[\"embeddings\"]) == 1\n    assert len(data[\"embeddings\"][0]) == 384  # MiniLM dimension\n\n    # Test batch\n    texts = [\"First sentence\", \"Second sentence\", \"Third one\"]\n    response = requests.post(url, json={\"texts\": texts})\n    assert response.status_code == 200\n    data = response.json()\n    assert len(data[\"embeddings\"]) == 3\n\n    # Test similarity (same text = same embedding)\n    response = requests.post(url, json={\"texts\": [\"test\", \"test\"]})\n    emb1, emb2 = response.json()[\"embeddings\"]\n    assert np.allclose(emb1, emb2, rtol=1e-5, atol=1e-6)\n\n    print(\"All tests passed!\")\n\n\nif __name__ == \"__main__\":\n    test_embedding_endpoint()\n"
  },
  {
    "path": "examples/example_config.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n# Sample Gunicorn configuration file.\n\n#\n# Server socket\n#\n#   bind - The socket to bind.\n#\n#       A string of the form: 'HOST', 'HOST:PORT', 'unix:PATH'.\n#       An IP is a valid HOST.\n#\n#   backlog - The number of pending connections. This refers\n#       to the number of clients that can be waiting to be\n#       served. Exceeding this number results in the client\n#       getting an error when attempting to connect. It should\n#       only affect servers under significant load.\n#\n#       Must be a positive integer. Generally set in the 64-2048\n#       range.\n#\n\nbind = '127.0.0.1:8000'\nbacklog = 2048\n\n#\n# Worker processes\n#\n#   workers - The number of worker processes that this server\n#       should keep alive for handling requests.\n#\n#       A positive integer generally in the 2-4 x $(NUM_CORES)\n#       range. You'll want to vary this a bit to find the best\n#       for your particular application's work load.\n#\n#   worker_class - The type of workers to use. The default\n#       sync class should handle most 'normal' types of work\n#       loads. You'll want to read\n#       https://gunicorn.org/design/#choosing-a-worker-type\n#       for information on when you might want to choose one\n#       of the other worker classes.\n#\n#       A string referring to a Python path to a subclass of\n#       gunicorn.workers.base.Worker. The default provided values\n#       can be seen at\n#       https://gunicorn.org/reference/settings/#worker_class\n#\n#   worker_connections - For the gevent and gthread worker classes\n#       this limits the maximum number of simultaneous clients that\n#       a single process can handle.\n#\n#       A positive integer generally set to around 1000.\n#\n#   timeout - If a worker does not notify the master process in this\n#       number of seconds it is killed and a new worker is spawned\n#       to replace it.\n#\n#       Generally set to thirty seconds. Only set this noticeably\n#       higher if you're sure of the repercussions for sync workers.\n#       For the non sync workers it just means that the worker\n#       process is still communicating and is not tied to the length\n#       of time required to handle a single request.\n#\n#   keepalive - The number of seconds to wait for the next request\n#       on a Keep-Alive HTTP connection.\n#\n#       A positive integer. Generally set in the 1-5 seconds range.\n#\n\nworkers = 1\nworker_class = 'sync'\nworker_connections = 1000\ntimeout = 30\nkeepalive = 2\n\n#\n#   spew - Install a trace function that spews every line of Python\n#       that is executed when running the server. This is the\n#       nuclear option.\n#\n#       True or False\n#\n\nspew = False\n\n#\n# Server mechanics\n#\n#   daemon - Detach the main Gunicorn process from the controlling\n#       terminal with a standard fork/fork sequence.\n#\n#       True or False\n#\n#   raw_env - Pass environment variables to the execution environment.\n#\n#   pidfile - The path to a pid file to write\n#\n#       A path string or None to not write a pid file.\n#\n#   user - Switch worker processes to run as this user.\n#\n#       A valid user id (as an integer) or the name of a user that\n#       can be retrieved with a call to pwd.getpwnam(value) or None\n#       to not change the worker process user.\n#\n#   group - Switch worker process to run as this group.\n#\n#       A valid group id (as an integer) or the name of a user that\n#       can be retrieved with a call to pwd.getgrnam(value) or None\n#       to change the worker processes group.\n#\n#   umask - A mask for file permissions written by Gunicorn. Note that\n#       this affects unix socket permissions.\n#\n#       A valid value for the os.umask(mode) call or a string\n#       compatible with int(value, 0) (0 means Python guesses\n#       the base, so values like \"0\", \"0xFF\", \"0022\" are valid\n#       for decimal, hex, and octal representations)\n#\n#   tmp_upload_dir - A directory to store temporary request data when\n#       requests are read. This will most likely be disappearing soon.\n#\n#       A path to a directory where the process owner can write. Or\n#       None to signal that Python should choose one on its own.\n#\n\ndaemon = False\nraw_env = [\n    'DJANGO_SECRET_KEY=something',\n    'SPAM=eggs',\n]\npidfile = None\numask = 0\nuser = None\ngroup = None\ntmp_upload_dir = None\n\n#\n#   Logging\n#\n#   logfile - The path to a log file to write to.\n#\n#       A path string. \"-\" means log to stdout.\n#\n#   loglevel - The granularity of log output\n#\n#       A string of \"debug\", \"info\", \"warning\", \"error\", \"critical\"\n#\n\nerrorlog = '-'\nloglevel = 'info'\naccesslog = '-'\naccess_log_format = '%(h)s %(l)s %(u)s %(t)s \"%(r)s\" %(s)s %(b)s \"%(f)s\" \"%(a)s\"'\n\n#\n# Process naming\n#\n#   proc_name - A base to use with setproctitle to change the way\n#       that Gunicorn processes are reported in the system process\n#       table. This affects things like 'ps' and 'top'. If you're\n#       going to be running more than one instance of Gunicorn you'll\n#       probably want to set a name to tell them apart. This requires\n#       that you install the setproctitle module.\n#\n#       A string or None to choose a default of something like 'gunicorn'.\n#\n\nproc_name = None\n\n#\n# Server hooks\n#\n#   post_fork - Called just after a worker has been forked.\n#\n#       A callable that takes a server and worker instance\n#       as arguments.\n#\n#   pre_fork - Called just prior to forking the worker subprocess.\n#\n#       A callable that accepts the same arguments as post_fork\n#\n#   pre_exec - Called just prior to forking off a secondary\n#       master process during things like config reloading.\n#\n#       A callable that takes a server instance as the sole argument.\n#\n\ndef post_fork(server, worker):\n    server.log.info(\"Worker spawned (pid: %s)\", worker.pid)\n\ndef pre_fork(server, worker):\n    pass\n\ndef pre_exec(server):\n    server.log.info(\"Forked child, re-executing.\")\n\ndef when_ready(server):\n    server.log.info(\"Server is ready. Spawning workers\")\n\ndef worker_int(worker):\n    worker.log.info(\"worker received INT or QUIT signal\")\n\n    ## get traceback info\n    import threading, sys, traceback\n    id2name = {th.ident: th.name for th in threading.enumerate()}\n    code = []\n    for threadId, stack in sys._current_frames().items():\n        code.append(\"\\n# Thread: %s(%d)\" % (id2name.get(threadId,\"\"),\n            threadId))\n        for filename, lineno, name, line in traceback.extract_stack(stack):\n            code.append('File: \"%s\", line %d, in %s' % (filename,\n                lineno, name))\n            if line:\n                code.append(\"  %s\" % (line.strip()))\n    worker.log.debug(\"\\n\".join(code))\n\ndef worker_abort(worker):\n    worker.log.info(\"worker received SIGABRT signal\")\n\ndef ssl_context(conf, default_ssl_context_factory):\n    import ssl\n\n    # The default SSLContext returned by the factory function is initialized\n    # with the TLS parameters from config, including TLS certificates and other\n    # parameters.\n    context = default_ssl_context_factory()\n\n    # The SSLContext can be further customized, for example by enforcing\n    # minimum TLS version.\n    context.minimum_version = ssl.TLSVersion.TLSv1_3\n\n    # Server can also return different server certificate depending which\n    # hostname the client uses. Requires Python 3.7 or later.\n    def sni_callback(socket, server_hostname, context):\n        if server_hostname == \"foo.127.0.0.1.nip.io\":\n            new_context = default_ssl_context_factory()\n            new_context.load_cert_chain(certfile=\"foo.pem\", keyfile=\"foo-key.pem\")\n            socket.context = new_context\n\n    context.sni_callback = sni_callback\n\n    return context\n"
  },
  {
    "path": "examples/frameworks/cherryapp.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport cherrypy\n\n\nclass Root:\n    @cherrypy.expose\n    def index(self):\n        return 'Hello World!'\n\ncherrypy.config.update({'environment': 'embedded'})\n\napp = cherrypy.tree.mount(Root())\n"
  },
  {
    "path": "examples/frameworks/django/README",
    "content": "Applications to test Django support:\n\ntesting -> Django 1.4\n"
  },
  {
    "path": "examples/frameworks/django/testing/manage.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n#!/usr/bin/env python\nimport os, sys\n\nif __name__ == \"__main__\":\n    os.environ.setdefault(\"DJANGO_SETTINGS_MODULE\", \"testing.settings\")\n\n    from django.core.management import execute_from_command_line\n\n    execute_from_command_line(sys.argv)\n"
  },
  {
    "path": "examples/frameworks/django/testing/testing/__init__.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n"
  },
  {
    "path": "examples/frameworks/django/testing/testing/apps/__init__.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n"
  },
  {
    "path": "examples/frameworks/django/testing/testing/apps/someapp/__init__.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n"
  },
  {
    "path": "examples/frameworks/django/testing/testing/apps/someapp/middleware.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom multiprocessing import Process, Queue\nimport requests\n\n\ndef child_process(queue):\n    while True:\n        print(queue.get())\n        requests.get('http://requestb.in/15s95oz1')\n\n\nclass GunicornSubProcessTestMiddleware:\n    def __init__(self):\n        super().__init__()\n        self.queue = Queue()\n        self.process = Process(target=child_process, args=(self.queue,))\n        self.process.start()\n\n    def process_request(self, request):\n        self.queue.put(('REQUEST',))\n\n    def process_response(self, request, response):\n        self.queue.put(('RESPONSE', response.status_code))\n        return response\n"
  },
  {
    "path": "examples/frameworks/django/testing/testing/apps/someapp/models.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n"
  },
  {
    "path": "examples/frameworks/django/testing/testing/apps/someapp/templates/base.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n    <head>\n        <meta charset=\"utf-8\"/>\n        <title>gunicorn django example app</title>\n        <!--[if IE]>\n\n        <script>\n            // allow IE to recognize HTMl5 elements\n            document.createElement('section');\n            document.createElement('article');\n            document.createElement('aside');\n            document.createElement('footer');\n            document.createElement('header');\n            document.createElement('nav');\n            document.createElement('time');\n\n        </script>\n        <![endif]-->\n\n    </head>\n    <body>\n        <header id=\"top\">\n            <h1>test app</h1>\n        </header>\n\n        {% block content %}{% endblock %}\n\n\n\n\t<footer></footer>\n    </body>\n</html>\n"
  },
  {
    "path": "examples/frameworks/django/testing/testing/apps/someapp/templates/home.html",
    "content": "{% extends \"base.html\" %}\n\n{% block content %}\n<form method=\"post\" enctype='multipart/form-data'>\n        {% csrf_token %}\n        <table>\n        {{ form.as_table }}\n        </table>\n        <input type=\"submit\" id=\"submit\" value=\"submit\">\n    </form>\n\n\t<h2>Got</h2>\n\t{% if subject %}\n\t\t<p><strong>subject:</strong><br>{{ subject}}</p>\n\t\t<p><strong>message:</strong><br>{{ message }}</p>\n\t\t<p><strong>size:</strong><br>{{ size }}</p>\n\t{% endif %}\n{% endblock content %}\n"
  },
  {
    "path": "examples/frameworks/django/testing/testing/apps/someapp/tests.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nThis file demonstrates two different styles of tests (one doctest and one\nunittest). These will both pass when you run \"manage.py test\".\n\nReplace these with more appropriate tests for your application.\n\"\"\"\n\nfrom django.test import TestCase\n\nclass SimpleTest(TestCase):\n    def test_basic_addition(self):\n        \"\"\"\n        Tests that 1 + 1 always equals 2.\n        \"\"\"\n        self.assertEqual(1 + 1, 2)\n\n__test__ = {\"doctest\": \"\"\"\nAnother way to test that 1 + 1 is equal to 2.\n\n>>> 1 + 1 == 2\nTrue\n\"\"\"}\n"
  },
  {
    "path": "examples/frameworks/django/testing/testing/apps/someapp/urls.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom django.conf.urls import url\n\nfrom . import views\n\nurlpatterns = [\n    url(r'^acsv$', views.acsv),\n    url(r'^$', views.home),\n]\n"
  },
  {
    "path": "examples/frameworks/django/testing/testing/apps/someapp/views.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport csv\nimport io\nimport os\nfrom django import forms\nfrom django.http import HttpResponse\nfrom django.shortcuts import render\nfrom django.template import RequestContext\n\n\nclass MsgForm(forms.Form):\n    subject = forms.CharField(max_length=100)\n    message = forms.CharField()\n    f = forms.FileField()\n\n\ndef home(request):\n    from django.conf import settings\n    print(settings.SOME_VALUE)\n    subject = None\n    message = None\n    size = 0\n    print(request.META)\n    if request.POST:\n        form = MsgForm(request.POST, request.FILES)\n        print(request.FILES)\n        if form.is_valid():\n            subject = form.cleaned_data['subject']\n            message = form.cleaned_data['message']\n            f = request.FILES['f']\n\n            if not hasattr(f, \"fileno\"):\n                size = len(f.read())\n            else:\n                try:\n                    size = int(os.fstat(f.fileno())[6])\n                except io.UnsupportedOperation:\n                    size = len(f.read())\n    else:\n        form = MsgForm()\n\n\n\n    return render(request, 'home.html', {\n        'form': form,\n        'subject': subject,\n        'message': message,\n        'size': size\n    })\n\n\ndef acsv(request):\n    rows = [\n        {'a': 1, 'b': 2},\n        {'a': 3, 'b': 3}\n    ]\n\n    response = HttpResponse(mimetype='text/csv')\n    response['Content-Disposition'] = 'attachment; filename=report.csv'\n\n    writer = csv.writer(response)\n    writer.writerow(['a', 'b'])\n\n    for r in rows:\n        writer.writerow([r['a'], r['b']])\n\n    return response\n"
  },
  {
    "path": "examples/frameworks/django/testing/testing/settings.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n# Django settings for testing project.\n\nDEBUG = True\nTEMPLATE_DEBUG = DEBUG\n\nADMINS = (\n    # ('Your Name', 'your_email@example.com'),\n)\n\nMANAGERS = ADMINS\n\nDATABASES = {\n    'default': {\n        'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.\n        'NAME': 'testdb.sql',                      # Or path to database file if using sqlite3.\n        'USER': '',                      # Not used with sqlite3.\n        'PASSWORD': '',                  # Not used with sqlite3.\n        'HOST': '',                      # Set to empty string for localhost. Not used with sqlite3.\n        'PORT': '',                      # Set to empty string for default. Not used with sqlite3.\n    }\n}\n\n# Local time zone for this installation. Choices can be found here:\n# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name\n# although not all choices may be available on all operating systems.\n# On Unix systems, a value of None will cause Django to use the same\n# timezone as the operating system.\n# If running in a Windows environment this must be set to the same as your\n# system time zone.\nTIME_ZONE = 'America/Chicago'\n\n# Language code for this installation. All choices can be found here:\n# http://www.i18nguy.com/unicode/language-identifiers.html\nLANGUAGE_CODE = 'en-us'\n\nSITE_ID = 1\n\n# If you set this to False, Django will make some optimizations so as not\n# to load the internationalization machinery.\nUSE_I18N = True\n\n# If you set this to False, Django will not format dates, numbers and\n# calendars according to the current locale.\nUSE_L10N = True\n\n# If you set this to False, Django will not use timezone-aware datetimes.\nUSE_TZ = True\n\n# Absolute filesystem path to the directory that will hold user-uploaded files.\n# Example: \"/home/media/media.lawrence.com/media/\"\nMEDIA_ROOT = ''\n\n# URL that handles the media served from MEDIA_ROOT. Make sure to use a\n# trailing slash.\n# Examples: \"http://media.lawrence.com/media/\", \"http://example.com/media/\"\nMEDIA_URL = ''\n\n# Absolute path to the directory static files should be collected to.\n# Don't put anything in this directory yourself; store your static files\n# in apps' \"static/\" subdirectories and in STATICFILES_DIRS.\n# Example: \"/home/media/media.lawrence.com/static/\"\nSTATIC_ROOT = ''\n\n# URL prefix for static files.\n# Example: \"http://media.lawrence.com/static/\"\nSTATIC_URL = '/static/'\n\n# Additional locations of static files\nSTATICFILES_DIRS = (\n    # Put strings here, like \"/home/html/static\" or \"C:/www/django/static\".\n    # Always use forward slashes, even on Windows.\n    # Don't forget to use absolute paths, not relative paths.\n)\n\n# List of finder classes that know how to find static files in\n# various locations.\nSTATICFILES_FINDERS = (\n    'django.contrib.staticfiles.finders.FileSystemFinder',\n    'django.contrib.staticfiles.finders.AppDirectoriesFinder',\n#    'django.contrib.staticfiles.finders.DefaultStorageFinder',\n)\n\n# Make this unique, and don't share it with anybody.\nSECRET_KEY = 'what'\n\n\n# List of callables that know how to import templates from various sources.\nTEMPLATE_LOADERS = (\n    'django.template.loaders.filesystem.Loader',\n    'django.template.loaders.app_directories.Loader',\n#     'django.template.loaders.eggs.Loader',\n)\n\nMIDDLEWARE_CLASSES = (\n    'django.middleware.common.CommonMiddleware',\n    'django.contrib.sessions.middleware.SessionMiddleware',\n    'django.middleware.csrf.CsrfViewMiddleware',\n    'django.contrib.auth.middleware.AuthenticationMiddleware',\n    'django.contrib.messages.middleware.MessageMiddleware',\n    # Uncomment the next line for simple clickjacking protection:\n    # 'django.middleware.clickjacking.XFrameOptionsMiddleware',\n    # uncomment the next line to test multiprocessing\n    #'testing.apps.someapp.middleware.GunicornSubProcessTestMiddleware',\n)\n\nROOT_URLCONF = 'testing.urls'\n\n# Python dotted path to the WSGI application used by Django's runserver.\nWSGI_APPLICATION = 'testing.wsgi.application'\n\nTEMPLATES = [\n    {\n        'BACKEND': 'django.template.backends.django.DjangoTemplates',\n        'DIRS': [],\n        'APP_DIRS': True,\n        'OPTIONS': {\n            # ... some options here ...\n        },\n    },\n]\n\nTEMPLATE_DIRS = (\n    # Put strings here, like \"/home/html/django_templates\" or \"C:/www/django/templates\".\n    # Always use forward slashes, even on Windows.\n    # Don't forget to use absolute paths, not relative paths.\n)\n\nINSTALLED_APPS = (\n    'django.contrib.auth',\n    'django.contrib.contenttypes',\n    'django.contrib.sessions',\n    'django.contrib.sites',\n    'django.contrib.messages',\n    'django.contrib.staticfiles',\n    # Uncomment the next line to enable the admin:\n    'django.contrib.admin',\n    # Uncomment the next line to enable admin documentation:\n    # 'django.contrib.admindocs',\n    'testing.apps.someapp',\n    'gunicorn'\n)\n\n# A sample logging configuration. The only tangible logging\n# performed by this configuration is to send an email to\n# the site admins on every HTTP 500 error when DEBUG=False.\n# See http://docs.djangoproject.com/en/dev/topics/logging for\n# more details on how to customize your logging configuration.\nLOGGING = {\n    'version': 1,\n    'disable_existing_loggers': False,\n    'filters': {\n        'require_debug_false': {\n            '()': 'django.utils.log.RequireDebugFalse'\n        }\n    },\n    'handlers': {\n        'mail_admins': {\n            'level': 'ERROR',\n            'filters': ['require_debug_false'],\n            'class': 'django.utils.log.AdminEmailHandler'\n        }\n    },\n    'loggers': {\n        'django.request': {\n            'handlers': ['mail_admins'],\n            'level': 'ERROR',\n            'propagate': True,\n        },\n    }\n}\n\nSOME_VALUE = \"test on reload\"\n"
  },
  {
    "path": "examples/frameworks/django/testing/testing/urls.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom django.conf.urls import include, url\n\n# Uncomment the next two lines to enable the admin:\nfrom django.contrib import admin\nadmin.autodiscover()\n\nurlpatterns = [\n    # Examples:\n    # url(r'^$', 'testing.views.home', name='home'),\n    # url(r'^testing/', include('testing.foo.urls')),\n\n    # Uncomment the admin/doc line below to enable admin documentation:\n    # url(r'^admin/doc/', include('django.contrib.admindocs.urls')),\n\n    # Uncomment the next line to enable the admin:\n    url(r'^admin/', admin.site.urls),\n\n    url(r'^', include(\"testing.apps.someapp.urls\")),\n]\n"
  },
  {
    "path": "examples/frameworks/django/testing/testing/wsgi.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nWSGI config for testing project.\n\nThis module contains the WSGI application used by Django's development server\nand any production WSGI deployments. It should expose a module-level variable\nnamed ``application``. Django's ``runserver`` and ``runfcgi`` commands discover\nthis application via the ``WSGI_APPLICATION`` setting.\n\nUsually you will have the standard Django WSGI application here, but it also\nmight make sense to replace the whole Django WSGI application with a custom one\nthat later delegates to the Django one. For example, you could introduce WSGI\nmiddleware here, or combine a Django application with an application of another\nframework.\n\n\"\"\"\nimport os\nimport sys\n\n# make sure the current project is in PYTHONPATH\nsys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__),\n                                                \"..\")))\n\n# set the environment settings\nos.environ.setdefault(\"DJANGO_SETTINGS_MODULE\", \"testing.settings\")\n\n# This application object is used by any WSGI server configured to use this\n# file. This includes Django's development server, if the WSGI_APPLICATION\n# setting points here.\nfrom django.core.wsgi import get_wsgi_application\napplication = get_wsgi_application()\n\n# Apply WSGI middleware here.\n# from helloworld.wsgi import HelloWorldApplication\n# application = HelloWorldApplication(application)\n"
  },
  {
    "path": "examples/frameworks/flask_sendfile.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport io\n\nfrom flask import Flask, send_file\n\napp = Flask(__name__)\n\n@app.route('/')\ndef index():\n    buf = io.BytesIO()\n    buf.write(b'hello world')\n    buf.seek(0)\n    return send_file(buf,\n                     attachment_filename=\"testing.txt\",\n                     as_attachment=True)\n"
  },
  {
    "path": "examples/frameworks/flaskapp.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n# Run with:\n#\n#   $ gunicorn flaskapp:app\n#\n\nfrom flask import Flask\napp = Flask(__name__)\n\n@app.route(\"/\")\ndef hello():\n    return \"Hello World!\"\n"
  },
  {
    "path": "examples/frameworks/flaskapp_aiohttp_wsgi.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n# Example command to run the example:\n#\n#   $ gunicorn flaskapp_aiohttp_wsgi:aioapp -k aiohttp.worker.GunicornWebWorker\n#\n\nfrom aiohttp import web\nfrom aiohttp_wsgi import WSGIHandler\nfrom flask import Flask\n\napp = Flask(__name__)\n\n\n@app.route('/')\ndef hello():\n    return 'Hello, world!'\n\n\ndef make_aiohttp_app(app):\n    wsgi_handler = WSGIHandler(app)\n    aioapp = web.Application()\n    aioapp.router.add_route('*', '/{path_info:.*}', wsgi_handler)\n    return aioapp\n\naioapp = make_aiohttp_app(app)\n"
  },
  {
    "path": "examples/frameworks/pyramidapp.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom pyramid.config import Configurator\nfrom pyramid.response import Response\n\ndef hello_world(request):\n    return Response('Hello world!')\n\ndef goodbye_world(request):\n    return Response('Goodbye world!')\n\nconfig = Configurator()\nconfig.add_view(hello_world)\nconfig.add_view(goodbye_world, name='goodbye')\napp = config.make_wsgi_app()\n"
  },
  {
    "path": "examples/frameworks/requirements.txt",
    "content": "-r requirements_flaskapp.txt\n-r requirements_cherryapp.txt\n-r requirements_pyramidapp.txt\n-r requirements_tornadoapp.txt\n-r requirements_webpyapp.txt\n"
  },
  {
    "path": "examples/frameworks/requirements_cherryapp.txt",
    "content": "cherrypy\n"
  },
  {
    "path": "examples/frameworks/requirements_flaskapp.txt",
    "content": "flask\n"
  },
  {
    "path": "examples/frameworks/requirements_pyramidapp.txt",
    "content": "pyramid\n"
  },
  {
    "path": "examples/frameworks/requirements_tornadoapp.txt",
    "content": "tornado<6\n"
  },
  {
    "path": "examples/frameworks/requirements_webpyapp.txt",
    "content": "web-py\n"
  },
  {
    "path": "examples/frameworks/tornadoapp.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n#\n# Run with:\n#\n#   $ gunicorn -k tornado tornadoapp:app\n#\n\nimport asyncio\nimport tornado.ioloop\nimport tornado.web\n\n\nclass MainHandler(tornado.web.RequestHandler):\n    async def get(self):\n        # Your asynchronous code here\n        await asyncio.sleep(1)  # Example of an asynchronous operation\n        self.write(\"Hello, World!\")\n\n\ndef make_app():\n    return tornado.web.Application([\n        (r\"/\", MainHandler),\n    ])\n\n\napp = make_app()\n\n\nif __name__ == \"__main__\":\n    app.listen(8888)\n    tornado.ioloop.IOLoop.current().start()\n"
  },
  {
    "path": "examples/frameworks/webpyapp.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n# Run with\n#\n# $ gunicorn webpyapp:app\n#\n\nimport web\n\nurls = (\n    '/', 'index'\n)\n\nclass index:\n    def GET(self):\n        return \"Hello, world!\"\n\napp = web.application(urls, globals()).wsgifunc()\n"
  },
  {
    "path": "examples/gunicorn_rc",
    "content": "#!/bin/sh\n\nGUNICORN=/usr/local/bin/gunicorn\nROOT=/path/to/project\nPID=/var/run/gunicorn.pid\n\nAPP=main:application\n\nif [ -f $PID ]; then rm $PID; fi\n\ncd $ROOT\nexec $GUNICORN -c $ROOT/gunicorn.conf.py --pid=$PID $APP\n"
  },
  {
    "path": "examples/hello.txt",
    "content": "Hello world!\n"
  },
  {
    "path": "examples/http2_features/Dockerfile",
    "content": "FROM python:3.12-slim\n\nWORKDIR /app\n\n# Install h2 for HTTP/2 support and httpx for testing\nRUN pip install --no-cache-dir h2 httpx\n\n# Copy gunicorn source and install\nCOPY . /app/gunicorn-src\nRUN pip install /app/gunicorn-src\n\n# Copy example app\nCOPY examples/http2_features /app/http2_features\n\n# Copy SSL certificates\nCOPY examples/server.crt /app/certs/server.crt\nCOPY examples/server.key /app/certs/server.key\n\nENV PYTHONPATH=/app\n\nEXPOSE 8443\nCMD [\"gunicorn\", \"http2_features.http2_app:app\", \"-c\", \"http2_features/gunicorn_conf.py\"]\n"
  },
  {
    "path": "examples/http2_features/__init__.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n"
  },
  {
    "path": "examples/http2_features/docker-compose.yml",
    "content": "services:\n  http2-features:\n    build:\n      context: ../..\n      dockerfile: examples/http2_features/Dockerfile\n    ports:\n      - \"8443:8443\"\n    healthcheck:\n      test: [\"CMD\", \"python\", \"-c\", \"import httpx; httpx.get('https://127.0.0.1:8443/health', verify=False)\"]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n      start_period: 5s\n"
  },
  {
    "path": "examples/http2_features/gunicorn_conf.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n# Gunicorn configuration for HTTP/2 features example\n\nbind = \"0.0.0.0:8443\"\nworkers = 2\nworker_class = \"asgi\"\n\n# SSL configuration (required for HTTP/2)\ncertfile = \"/app/certs/server.crt\"\nkeyfile = \"/app/certs/server.key\"\n\n# HTTP/2 configuration\nhttp_protocols = \"h2,h1\"\nhttp2_max_concurrent_streams = 100\nhttp2_initial_window_size = 65535\nhttp2_max_frame_size = 16384\n\n# Logging\naccesslog = \"-\"\nerrorlog = \"-\"\nloglevel = \"info\"\n"
  },
  {
    "path": "examples/http2_features/http2_app.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nHTTP/2 ASGI application demonstrating priority and trailers.\n\nThis example shows how to:\n- Access stream priority information from HTTP/2 requests\n- Send response trailers (useful for gRPC, checksums, etc.)\n\nRun with:\n    cd examples/http2_features\n    docker compose up --build\n\nTest with:\n    python test_http2.py\n\nOr manually:\n    curl -k --http2 https://localhost:8443/\n    curl -k --http2 https://localhost:8443/priority\n    curl -k --http2 https://localhost:8443/trailers\n\"\"\"\n\nimport json\nimport hashlib\n\n\nasync def app(scope, receive, send):\n    \"\"\"ASGI application demonstrating HTTP/2 priority and trailers.\"\"\"\n\n    if scope[\"type\"] == \"lifespan\":\n        await handle_lifespan(scope, receive, send)\n    elif scope[\"type\"] == \"http\":\n        await handle_http(scope, receive, send)\n    else:\n        raise ValueError(f\"Unknown scope type: {scope['type']}\")\n\n\nasync def handle_lifespan(scope, receive, send):\n    \"\"\"Handle lifespan events (startup/shutdown).\"\"\"\n    while True:\n        message = await receive()\n        if message[\"type\"] == \"lifespan.startup\":\n            print(\"HTTP/2 features app starting...\")\n            await send({\"type\": \"lifespan.startup.complete\"})\n        elif message[\"type\"] == \"lifespan.shutdown\":\n            print(\"HTTP/2 features app shutting down...\")\n            await send({\"type\": \"lifespan.shutdown.complete\"})\n            return\n\n\nasync def handle_http(scope, receive, send):\n    \"\"\"Route HTTP requests to handlers.\"\"\"\n    path = scope[\"path\"]\n    method = scope[\"method\"]\n\n    if path == \"/\" and method == \"GET\":\n        await handle_index(scope, receive, send)\n    elif path == \"/priority\" and method == \"GET\":\n        await handle_priority(scope, receive, send)\n    elif path == \"/trailers\" and method in (\"GET\", \"POST\"):\n        await handle_trailers(scope, receive, send)\n    elif path == \"/combined\" and method in (\"GET\", \"POST\"):\n        await handle_combined(scope, receive, send)\n    elif path == \"/health\" and method == \"GET\":\n        await send_response(send, 200, b\"OK\")\n    else:\n        await send_response(send, 404, b\"Not Found\\n\")\n\n\nasync def handle_index(scope, receive, send):\n    \"\"\"Show available endpoints and HTTP/2 features.\"\"\"\n    extensions = scope.get(\"extensions\", {})\n    http_version = scope.get(\"http_version\", \"1.1\")\n\n    info = {\n        \"message\": \"HTTP/2 Features Demo\",\n        \"http_version\": http_version,\n        \"endpoints\": {\n            \"/\": \"This info page\",\n            \"/priority\": \"Shows stream priority information\",\n            \"/trailers\": \"Demonstrates response trailers with checksum\",\n            \"/combined\": \"Shows both priority and trailers\",\n            \"/health\": \"Health check endpoint\",\n        },\n        \"extensions\": list(extensions.keys()),\n    }\n\n    body = json.dumps(info, indent=2).encode() + b\"\\n\"\n    await send_response(send, 200, body, content_type=b\"application/json\")\n\n\nasync def handle_priority(scope, receive, send):\n    \"\"\"Return stream priority information.\n\n    HTTP/2 allows clients to indicate relative importance of requests.\n    Gunicorn exposes this through the http.response.priority extension.\n    \"\"\"\n    extensions = scope.get(\"extensions\", {})\n    priority_info = extensions.get(\"http.response.priority\")\n\n    if priority_info:\n        response = {\n            \"http_version\": scope.get(\"http_version\", \"1.1\"),\n            \"priority\": {\n                \"weight\": priority_info[\"weight\"],\n                \"depends_on\": priority_info[\"depends_on\"],\n                \"description\": (\n                    f\"Weight {priority_info['weight']}/256 - \"\n                    f\"{'high' if priority_info['weight'] > 128 else 'normal' if priority_info['weight'] > 64 else 'low'} priority\"\n                ),\n            },\n            \"note\": \"Priority is advisory - use for scheduling hints\",\n        }\n    else:\n        response = {\n            \"http_version\": scope.get(\"http_version\", \"1.1\"),\n            \"priority\": None,\n            \"note\": \"Priority information only available for HTTP/2 requests\",\n        }\n\n    body = json.dumps(response, indent=2).encode() + b\"\\n\"\n    await send_response(send, 200, body, content_type=b\"application/json\")\n\n\nasync def handle_trailers(scope, receive, send):\n    \"\"\"Demonstrate response trailers.\n\n    Trailers are headers sent after the response body.\n    Common uses: gRPC status codes, checksums, timing info.\n    \"\"\"\n    extensions = scope.get(\"extensions\", {})\n    supports_trailers = \"http.response.trailers\" in extensions\n\n    # Read request body if POST\n    body_data = b\"\"\n    if scope[\"method\"] == \"POST\":\n        body_data = await read_body(receive)\n\n    # Generate response\n    response_body = body_data if body_data else b\"Hello from HTTP/2 with trailers!\\n\"\n\n    # Calculate checksum for trailer\n    checksum = hashlib.md5(response_body).hexdigest()\n\n    if supports_trailers:\n        # Send response announcing trailers\n        await send({\n            \"type\": \"http.response.start\",\n            \"status\": 200,\n            \"headers\": [\n                (b\"content-type\", b\"application/octet-stream\"),\n                (b\"trailer\", b\"content-md5, x-processing-time\"),\n            ],\n        })\n\n        # Send body\n        await send({\n            \"type\": \"http.response.body\",\n            \"body\": response_body,\n            \"more_body\": False,\n        })\n\n        # Send trailers\n        await send({\n            \"type\": \"http.response.trailers\",\n            \"headers\": [\n                (b\"content-md5\", checksum.encode()),\n                (b\"x-processing-time\", b\"42ms\"),\n            ],\n        })\n    else:\n        # HTTP/1.1 fallback - include checksum in regular headers\n        response = {\n            \"message\": \"Trailers not supported (HTTP/1.1)\",\n            \"data\": response_body.decode(\"utf-8\", errors=\"replace\"),\n            \"checksum_in_header\": checksum,\n        }\n        body = json.dumps(response, indent=2).encode() + b\"\\n\"\n        await send_response(\n            send, 200, body,\n            content_type=b\"application/json\",\n            extra_headers=[(b\"x-checksum\", checksum.encode())]\n        )\n\n\nasync def handle_combined(scope, receive, send):\n    \"\"\"Show both priority and trailers in one response.\n\n    This demonstrates a realistic scenario like gRPC where\n    priority affects scheduling and trailers carry status.\n    \"\"\"\n    extensions = scope.get(\"extensions\", {})\n    priority_info = extensions.get(\"http.response.priority\")\n    supports_trailers = \"http.response.trailers\" in extensions\n\n    # Build response showing all HTTP/2 features\n    response = {\n        \"http_version\": scope.get(\"http_version\", \"1.1\"),\n        \"priority\": None,\n        \"trailers_supported\": supports_trailers,\n    }\n\n    if priority_info:\n        response[\"priority\"] = {\n            \"weight\": priority_info[\"weight\"],\n            \"depends_on\": priority_info[\"depends_on\"],\n        }\n\n    response_body = json.dumps(response, indent=2).encode() + b\"\\n\"\n    checksum = hashlib.md5(response_body).hexdigest()\n\n    if supports_trailers:\n        # Full HTTP/2 response with trailers\n        await send({\n            \"type\": \"http.response.start\",\n            \"status\": 200,\n            \"headers\": [\n                (b\"content-type\", b\"application/json\"),\n                (b\"trailer\", b\"content-md5, x-status\"),\n            ],\n        })\n\n        await send({\n            \"type\": \"http.response.body\",\n            \"body\": response_body,\n            \"more_body\": False,\n        })\n\n        await send({\n            \"type\": \"http.response.trailers\",\n            \"headers\": [\n                (b\"content-md5\", checksum.encode()),\n                (b\"x-status\", b\"success\"),\n            ],\n        })\n    else:\n        await send_response(send, 200, response_body, content_type=b\"application/json\")\n\n\nasync def send_response(send, status, body, content_type=b\"text/plain\", extra_headers=None):\n    \"\"\"Send a simple HTTP response.\"\"\"\n    headers = [\n        (b\"content-type\", content_type),\n        (b\"content-length\", str(len(body)).encode()),\n    ]\n    if extra_headers:\n        headers.extend(extra_headers)\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": status,\n        \"headers\": headers,\n    })\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": body,\n    })\n\n\nasync def read_body(receive):\n    \"\"\"Read the full request body.\"\"\"\n    body = b\"\"\n    while True:\n        message = await receive()\n        body += message.get(\"body\", b\"\")\n        if not message.get(\"more_body\", False):\n            break\n    return body\n"
  },
  {
    "path": "examples/http2_features/requirements.txt",
    "content": "# Requirements for testing HTTP/2 features\nhttpx>=0.24.0\n"
  },
  {
    "path": "examples/http2_features/test_http2.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n#!/usr/bin/env python\n\"\"\"\nTest script for HTTP/2 features example.\n\nThis script tests:\n- HTTP/2 connection establishment\n- Stream priority access\n- Response trailers\n\nRun the server first:\n    docker compose up --build\n\nThen run tests:\n    python test_http2.py\n\nOr run directly against local server:\n    python test_http2.py --url https://localhost:8443\n\"\"\"\n\nimport argparse\nimport json\nimport ssl\nimport socket\nimport sys\nfrom urllib.parse import urlparse\n\n\ndef create_h2_connection(host, port):\n    \"\"\"Create an HTTP/2 connection using the h2 library.\"\"\"\n    try:\n        import h2.connection\n        import h2.config\n    except ImportError:\n        print(\"Please install h2: pip install h2\")\n        sys.exit(1)\n\n    # Create socket with SSL\n    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n    ctx = ssl.create_default_context()\n    ctx.check_hostname = False\n    ctx.verify_mode = ssl.CERT_NONE\n    ctx.set_alpn_protocols(['h2'])\n\n    sock = ctx.wrap_socket(sock, server_hostname=host)\n    sock.connect((host, port))\n    sock.settimeout(10.0)\n\n    # Verify ALPN\n    alpn = sock.selected_alpn_protocol()\n    if alpn != 'h2':\n        raise RuntimeError(f\"HTTP/2 not negotiated, got: {alpn}\")\n\n    # Create h2 connection\n    config = h2.config.H2Configuration(client_side=True)\n    h2_conn = h2.connection.H2Connection(config=config)\n    h2_conn.initiate_connection()\n    sock.sendall(h2_conn.data_to_send())\n\n    # Receive server settings\n    data = sock.recv(65536)\n    h2_conn.receive_data(data)\n    sock.sendall(h2_conn.data_to_send())\n\n    return sock, h2_conn\n\n\ndef h2_request(sock, h2_conn, stream_id, method, path, authority):\n    \"\"\"Make an HTTP/2 request and return the response.\"\"\"\n    import h2.events\n\n    # Send request\n    h2_conn.send_headers(stream_id, [\n        (':method', method),\n        (':path', path),\n        (':authority', authority),\n        (':scheme', 'https'),\n    ], end_stream=True)\n    sock.sendall(h2_conn.data_to_send())\n\n    # Collect response\n    status = None\n    headers = {}\n    body = b''\n    trailers = {}\n\n    while True:\n        data = sock.recv(65536)\n        if not data:\n            break\n\n        events = h2_conn.receive_data(data)\n        to_send = h2_conn.data_to_send()\n        if to_send:\n            sock.sendall(to_send)\n\n        for event in events:\n            if isinstance(event, h2.events.ResponseReceived):\n                if event.stream_id == stream_id:\n                    for name, value in event.headers:\n                        if name == b':status':\n                            status = int(value.decode())\n                        else:\n                            headers[name.decode()] = value.decode()\n\n            elif isinstance(event, h2.events.DataReceived):\n                if event.stream_id == stream_id:\n                    body += event.data\n\n            elif isinstance(event, h2.events.TrailersReceived):\n                if event.stream_id == stream_id:\n                    for name, value in event.headers:\n                        trailers[name.decode()] = value.decode()\n\n            elif isinstance(event, h2.events.StreamEnded):\n                if event.stream_id == stream_id:\n                    return {\n                        'status': status,\n                        'headers': headers,\n                        'body': body,\n                        'trailers': trailers,\n                    }\n\n            elif isinstance(event, h2.events.ConnectionTerminated):\n                raise RuntimeError(f\"Connection terminated: {event.error_code}\")\n\n    return None\n\n\ndef test_http2_connection(host, port):\n    \"\"\"Test that HTTP/2 is negotiated.\"\"\"\n    print(\"\\n=== Testing HTTP/2 Connection ===\")\n\n    try:\n        sock, h2_conn = create_h2_connection(host, port)\n        print(\"HTTP/2 connection established successfully!\")\n\n        response = h2_request(sock, h2_conn, 1, 'GET', '/', f'{host}:{port}')\n        print(f\"Status: {response['status']}\")\n\n        data = json.loads(response['body'].decode())\n        print(f\"Extensions available: {data.get('extensions', [])}\")\n\n        sock.close()\n        return response['status'] == 200\n    except Exception as e:\n        print(f\"ERROR: {e}\")\n        return False\n\n\ndef test_priority(host, port):\n    \"\"\"Test stream priority endpoint.\"\"\"\n    print(\"\\n=== Testing Stream Priority ===\")\n\n    try:\n        sock, h2_conn = create_h2_connection(host, port)\n\n        response = h2_request(sock, h2_conn, 1, 'GET', '/priority', f'{host}:{port}')\n        print(f\"Status: {response['status']}\")\n\n        data = json.loads(response['body'].decode())\n        print(f\"Priority info: {data.get('priority')}\")\n\n        if data.get(\"priority\"):\n            print(f\"  Weight: {data['priority']['weight']}\")\n            print(f\"  Depends on: {data['priority']['depends_on']}\")\n\n        sock.close()\n        return response['status'] == 200 and data.get(\"priority\") is not None\n    except Exception as e:\n        print(f\"ERROR: {e}\")\n        return False\n\n\ndef test_trailers(host, port):\n    \"\"\"Test response trailers.\"\"\"\n    print(\"\\n=== Testing Response Trailers ===\")\n\n    try:\n        sock, h2_conn = create_h2_connection(host, port)\n\n        response = h2_request(sock, h2_conn, 1, 'GET', '/trailers', f'{host}:{port}')\n        print(f\"Status: {response['status']}\")\n        print(f\"Headers: {response['headers']}\")\n\n        if response['trailers']:\n            print(f\"Trailers received: {response['trailers']}\")\n            if 'content-md5' in response['trailers']:\n                print(f\"  Content-MD5: {response['trailers']['content-md5']}\")\n        else:\n            print(\"Note: No trailers received (client may not have advertised support)\")\n\n        sock.close()\n        return response['status'] == 200\n    except Exception as e:\n        print(f\"ERROR: {e}\")\n        return False\n\n\ndef test_combined(host, port):\n    \"\"\"Test combined priority and trailers.\"\"\"\n    print(\"\\n=== Testing Combined Features ===\")\n\n    try:\n        sock, h2_conn = create_h2_connection(host, port)\n\n        response = h2_request(sock, h2_conn, 1, 'GET', '/combined', f'{host}:{port}')\n        print(f\"Status: {response['status']}\")\n\n        data = json.loads(response['body'].decode())\n        print(f\"Response: {json.dumps(data, indent=2)}\")\n\n        if response['trailers']:\n            print(f\"Trailers: {response['trailers']}\")\n\n        sock.close()\n        return response['status'] == 200\n    except Exception as e:\n        print(f\"ERROR: {e}\")\n        return False\n\n\ndef test_multiple_streams(host, port):\n    \"\"\"Test multiple requests on the same connection.\"\"\"\n    print(\"\\n=== Testing Multiple Streams ===\")\n\n    try:\n        sock, h2_conn = create_h2_connection(host, port)\n\n        # Make multiple requests on the same connection\n        paths = ['/', '/priority', '/trailers', '/combined']\n        for i, path in enumerate(paths):\n            stream_id = i * 2 + 1  # Odd numbers for client-initiated streams\n            response = h2_request(sock, h2_conn, stream_id, 'GET', path, f'{host}:{port}')\n            print(f\"  {path}: {response['status']}\")\n\n        sock.close()\n        return True\n    except Exception as e:\n        print(f\"ERROR: {e}\")\n        return False\n\n\ndef main():\n    parser = argparse.ArgumentParser(description=\"Test HTTP/2 features\")\n    parser.add_argument(\n        \"--url\",\n        default=\"https://localhost:8443\",\n        help=\"Base URL of the server (default: https://localhost:8443)\"\n    )\n    args = parser.parse_args()\n\n    parsed = urlparse(args.url)\n    host = parsed.hostname or 'localhost'\n    port = parsed.port or 8443\n\n    print(f\"Testing against: {host}:{port}\")\n\n    results = []\n\n    try:\n        results.append((\"HTTP/2 Connection\", test_http2_connection(host, port)))\n        results.append((\"Stream Priority\", test_priority(host, port)))\n        results.append((\"Response Trailers\", test_trailers(host, port)))\n        results.append((\"Combined Features\", test_combined(host, port)))\n        results.append((\"Multiple Streams\", test_multiple_streams(host, port)))\n    except ConnectionRefusedError:\n        print(f\"\\nConnection refused to {host}:{port}\")\n        print(\"Make sure the server is running: docker compose up --build\")\n        return 1\n    except Exception as e:\n        print(f\"\\nUnexpected error: {e}\")\n        return 1\n\n    print(\"\\n=== Test Results ===\")\n    all_passed = True\n    for name, passed in results:\n        status = \"PASS\" if passed else \"FAIL\"\n        print(f\"  {name}: {status}\")\n        if not passed:\n            all_passed = False\n\n    if all_passed:\n        print(\"\\nAll tests passed!\")\n        return 0\n    else:\n        print(\"\\nSome tests failed.\")\n        return 1\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n"
  },
  {
    "path": "examples/http2_gevent/.gitignore",
    "content": "# Generated certificates - run ./generate_certs.sh to create\ncerts/\n"
  },
  {
    "path": "examples/http2_gevent/Dockerfile",
    "content": "# HTTP/2 with Gevent Example\n#\n# Build: docker build -t gunicorn-http2-gevent .\n# Run:   docker run -p 8443:8443 -v $(pwd)/certs:/certs:ro gunicorn-http2-gevent\n\nFROM python:3.12-slim\n\n# Install build dependencies for gevent and h2\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n    gcc \\\n    libc-dev \\\n    libffi-dev \\\n    && rm -rf /var/lib/apt/lists/*\n\nWORKDIR /app\n\n# Copy gunicorn source and install with gevent and http2 support\n# For production, use: pip install gunicorn[gevent,http2]\nCOPY --chown=root:root . /gunicorn-src/\nRUN pip install --no-cache-dir /gunicorn-src/[gevent,http2]\n\n# Copy application files\nCOPY examples/http2_gevent/app.py /app/\nCOPY examples/http2_gevent/gunicorn_conf.py /app/\n\n# Create non-root user for security\nRUN useradd -m -u 1000 gunicorn && \\\n    chown -R gunicorn:gunicorn /app\nUSER gunicorn\n\nEXPOSE 8443\n\n# Health check\nHEALTHCHECK --interval=10s --timeout=5s --start-period=5s --retries=3 \\\n    CMD python -c \"import ssl,socket; s=socket.socket(); s.settimeout(2); ctx=ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE; ss=ctx.wrap_socket(s,server_hostname='localhost'); ss.connect(('localhost',8443)); ss.close()\" || exit 1\n\n# Run gunicorn with the config file\nCMD [\"gunicorn\", \"--config\", \"gunicorn_conf.py\", \"app:app\"]\n"
  },
  {
    "path": "examples/http2_gevent/README.md",
    "content": "# HTTP/2 with Gevent Worker Example\n\nThis example demonstrates how to run Gunicorn with HTTP/2 support using the gevent async worker.\n\n## Features\n\n- HTTP/2 protocol with ALPN negotiation\n- Gevent-based async worker for high concurrency\n- Connection multiplexing (multiple streams per connection)\n- Flow control for large transfers\n- SSL/TLS encryption (required for HTTP/2)\n\n## Quick Start\n\n### 1. Generate SSL Certificates\n\nHTTP/2 requires TLS. Generate self-signed certificates for testing:\n\n```bash\nchmod +x generate_certs.sh\n./generate_certs.sh\n```\n\n### 2. Start with Docker Compose\n\n```bash\ndocker compose up -d\n```\n\n### 3. Test the Server\n\nUsing curl with HTTP/2:\n\n```bash\n# Basic request\ncurl -k --http2 https://localhost:8443/\n\n# Check HTTP version\ncurl -k --http2 -w \"HTTP Version: %{http_version}\\n\" https://localhost:8443/\n\n# Test echo endpoint\ncurl -k --http2 -X POST -d \"Hello HTTP/2\" https://localhost:8443/echo\n\n# Get server info\ncurl -k --http2 https://localhost:8443/info | jq\n```\n\n### 4. Run Tests\n\n```bash\n# Install test dependencies\npip install httpx[http2] pytest pytest-asyncio\n\n# Run tests\npython test_http2_gevent.py\n\n# Or with pytest for more detail\npytest test_http2_gevent.py -v\n```\n\n## Running Locally (Without Docker)\n\n### Prerequisites\n\n```bash\npip install gunicorn[gevent,http2]\n```\n\n### Generate Certificates\n\n```bash\n./generate_certs.sh\n```\n\n### Start Server\n\n```bash\ngunicorn --config gunicorn_conf.py app:app\n```\n\nOr with command-line options:\n\n```bash\ngunicorn app:app \\\n    --bind 0.0.0.0:8443 \\\n    --worker-class gevent \\\n    --workers 4 \\\n    --worker-connections 1000 \\\n    --http-protocols h2,h1 \\\n    --certfile certs/server.crt \\\n    --keyfile certs/server.key\n```\n\n## Configuration Options\n\n### HTTP/2 Settings\n\n| Setting | Default | Description |\n|---------|---------|-------------|\n| `http_protocols` | `['h1']` | Enable protocols: `['h2', 'h1']` for HTTP/2 |\n| `http2_max_concurrent_streams` | 100 | Max streams per connection |\n| `http2_initial_window_size` | 65535 | Flow control window size (bytes) |\n| `http2_max_frame_size` | 16384 | Max frame size (bytes) |\n| `http2_max_header_list_size` | 65536 | Max header list size (bytes) |\n\n### Gevent Worker Settings\n\n| Setting | Default | Description |\n|---------|---------|-------------|\n| `worker_class` | `sync` | Set to `gevent` for async |\n| `workers` | 1 | Number of worker processes |\n| `worker_connections` | 1000 | Max clients per worker |\n\n## Endpoints\n\n| Path | Method | Description |\n|------|--------|-------------|\n| `/` | GET | Hello message |\n| `/health` | GET | Health check |\n| `/echo` | POST | Echo request body |\n| `/info` | GET | Server/request info as JSON |\n| `/large` | GET | 1MB response (test streaming) |\n| `/stream` | GET | Server-sent events stream |\n| `/delay?seconds=N` | GET | Delayed response |\n| `/priority` | GET | HTTP/2 priority info |\n\n## Performance Tips\n\n1. **Worker Count**: Use `2 * CPU cores + 1` workers for I/O-bound apps\n2. **Connections**: Increase `worker_connections` for high concurrency\n3. **Window Size**: Larger `http2_initial_window_size` improves throughput for large transfers\n4. **Streams**: Increase `http2_max_concurrent_streams` for many parallel requests\n\n## Troubleshooting\n\n### Certificate Issues\n\n```bash\n# Regenerate certificates\nrm -rf certs/\n./generate_certs.sh\n```\n\n### Connection Refused\n\n```bash\n# Check if server is running\ndocker compose ps\n\n# View logs\ndocker compose logs -f\n```\n\n### HTTP/2 Not Negotiated\n\nEnsure:\n- SSL/TLS is configured (certfile and keyfile)\n- `http_protocols` includes `'h2'`\n- Client supports HTTP/2 over TLS (curl with `--http2`, not `--http2-prior-knowledge`)\n\n## License\n\nMIT License - See the main Gunicorn repository for details.\n"
  },
  {
    "path": "examples/http2_gevent/app.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nExample WSGI application demonstrating HTTP/2 with gevent worker.\n\nThis application showcases various HTTP/2 features including:\n- Basic request/response handling\n- Large file transfers (streaming)\n- Concurrent requests (multiplexing)\n- Server push simulation\n\"\"\"\n\nimport json\nimport time\n\n\ndef app(environ, start_response):\n    \"\"\"WSGI application for HTTP/2 demonstration.\"\"\"\n    path = environ.get('PATH_INFO', '/')\n    method = environ.get('REQUEST_METHOD', 'GET')\n\n    # Root endpoint\n    if path == '/':\n        body = b'Hello from HTTP/2 with Gevent!'\n        status = '200 OK'\n        content_type = 'text/plain; charset=utf-8'\n\n    # Health check\n    elif path == '/health':\n        body = b'OK'\n        status = '200 OK'\n        content_type = 'text/plain'\n\n    # Echo endpoint - returns the request body\n    elif path == '/echo':\n        content_length = int(environ.get('CONTENT_LENGTH', 0) or 0)\n        body = environ['wsgi.input'].read(content_length)\n        status = '200 OK'\n        content_type = 'application/octet-stream'\n\n    # JSON endpoint - returns request info as JSON\n    elif path == '/info':\n        info = {\n            'method': method,\n            'path': path,\n            'protocol': environ.get('SERVER_PROTOCOL', 'unknown'),\n            'http_version': environ.get('HTTP_VERSION', '1.1'),\n            'server': 'gunicorn with gevent + HTTP/2',\n            'headers': {\n                k: v for k, v in environ.items()\n                if k.startswith('HTTP_')\n            }\n        }\n        body = json.dumps(info, indent=2).encode('utf-8')\n        status = '200 OK'\n        content_type = 'application/json'\n\n    # Large response for testing streaming/flow control\n    elif path == '/large':\n        # Return 1MB of data\n        size = 1024 * 1024\n        body = b'X' * size\n        status = '200 OK'\n        content_type = 'application/octet-stream'\n\n    # Streaming response using generator\n    elif path == '/stream':\n        def generate():\n            for i in range(10):\n                yield f'data: chunk {i}\\n\\n'.encode('utf-8')\n                # Small delay to simulate streaming\n                time.sleep(0.1)\n\n        start_response('200 OK', [\n            ('Content-Type', 'text/event-stream'),\n            ('Cache-Control', 'no-cache'),\n        ])\n        return generate()\n\n    # Concurrent test endpoint with configurable delay\n    elif path.startswith('/delay'):\n        query = environ.get('QUERY_STRING', '')\n        try:\n            delay = float(query.split('=')[1]) if '=' in query else 0.5\n            delay = min(delay, 5.0)  # Cap at 5 seconds\n        except (ValueError, IndexError):\n            delay = 0.5\n\n        # Use gevent sleep for cooperative yielding\n        try:\n            import gevent\n            gevent.sleep(delay)\n        except ImportError:\n            time.sleep(delay)\n\n        body = f'Delayed response after {delay}s'.encode('utf-8')\n        status = '200 OK'\n        content_type = 'text/plain'\n\n    # HTTP/2 priority information (if available)\n    elif path == '/priority':\n        priority_info = {\n            'weight': environ.get('HTTP2_PRIORITY_WEIGHT', 'N/A'),\n            'depends_on': environ.get('HTTP2_PRIORITY_DEPENDS_ON', 'N/A'),\n            'exclusive': environ.get('HTTP2_PRIORITY_EXCLUSIVE', 'N/A'),\n        }\n        body = json.dumps(priority_info, indent=2).encode('utf-8')\n        status = '200 OK'\n        content_type = 'application/json'\n\n    # 404 for unknown paths\n    else:\n        body = b'Not Found'\n        status = '404 Not Found'\n        content_type = 'text/plain'\n\n    response_headers = [\n        ('Content-Type', content_type),\n        ('Content-Length', str(len(body))),\n        ('X-Worker-Type', 'gevent'),\n    ]\n\n    start_response(status, response_headers)\n    return [body]\n\n\n# Allow running directly for testing\nif __name__ == '__main__':\n    from wsgiref.simple_server import make_server\n    server = make_server('localhost', 8000, app)\n    print('Test server running on http://localhost:8000')\n    server.serve_forever()\n"
  },
  {
    "path": "examples/http2_gevent/docker-compose.yml",
    "content": "# HTTP/2 with Gevent Docker Compose\n#\n# Usage:\n#   # Generate certificates first (or use your own)\n#   ./generate_certs.sh\n#\n#   # Start services\n#   docker compose up -d\n#\n#   # Test with curl (requires curl with HTTP/2 support)\n#   curl -k --http2 https://localhost:8443/\n#\n#   # View logs\n#   docker compose logs -f\n#\n#   # Stop services\n#   docker compose down\n\nservices:\n  gunicorn:\n    build:\n      context: ../..\n      dockerfile: examples/http2_gevent/Dockerfile\n    ports:\n      - \"8443:8443\"\n    volumes:\n      - ./certs:/certs:ro\n    environment:\n      - GUNICORN_WORKERS=4\n      - GUNICORN_LOG_LEVEL=info\n    healthcheck:\n      test: [\"CMD\", \"python\", \"-c\", \"import ssl,socket; s=socket.socket(); s.settimeout(2); ctx=ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE; ss=ctx.wrap_socket(s,server_hostname='localhost'); ss.connect(('localhost',8443)); ss.close()\"]\n      interval: 5s\n      timeout: 5s\n      retries: 10\n      start_period: 10s\n    restart: unless-stopped\n    deploy:\n      resources:\n        limits:\n          cpus: '2'\n          memory: 512M\n\nnetworks:\n  default:\n    driver: bridge\n"
  },
  {
    "path": "examples/http2_gevent/generate_certs.sh",
    "content": "#!/bin/bash\n#\n# Generate self-signed certificates for HTTP/2 testing.\n#\n# Usage: ./generate_certs.sh\n#\n\nset -e\n\nCERTS_DIR=\"./certs\"\nCERT_FILE=\"$CERTS_DIR/server.crt\"\nKEY_FILE=\"$CERTS_DIR/server.key\"\n\n# Create certs directory if it doesn't exist\nmkdir -p \"$CERTS_DIR\"\n\n# Check if certificates already exist\nif [ -f \"$CERT_FILE\" ] && [ -f \"$KEY_FILE\" ]; then\n    echo \"Certificates already exist in $CERTS_DIR\"\n    echo \"Delete them first if you want to regenerate.\"\n    exit 0\nfi\n\necho \"Generating self-signed certificate...\"\n\nopenssl req -x509 -newkey rsa:2048 \\\n    -keyout \"$KEY_FILE\" \\\n    -out \"$CERT_FILE\" \\\n    -days 365 \\\n    -nodes \\\n    -subj \"/CN=localhost/O=Gunicorn HTTP2 Example/C=US\" \\\n    -addext \"subjectAltName=DNS:localhost,DNS:gunicorn,IP:127.0.0.1\"\n\n# Set appropriate permissions\nchmod 644 \"$CERT_FILE\"\nchmod 600 \"$KEY_FILE\"\n\necho \"Certificates generated successfully:\"\necho \"  Certificate: $CERT_FILE\"\necho \"  Private Key: $KEY_FILE\"\necho \"\"\necho \"You can now start the server with:\"\necho \"  docker compose up -d\"\necho \"\"\necho \"Or run locally with:\"\necho \"  gunicorn --config gunicorn_conf.py app:app\"\n"
  },
  {
    "path": "examples/http2_gevent/gunicorn_conf.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nGunicorn configuration for HTTP/2 with gevent worker.\n\nThis configuration demonstrates:\n- HTTP/2 protocol support with ALPN\n- Gevent async worker for high concurrency\n- SSL/TLS configuration\n- HTTP/2 specific tuning options\n\"\"\"\n\nimport os\nimport multiprocessing\n\n# Server socket\nbind = os.environ.get('GUNICORN_BIND', '0.0.0.0:8443')\n\n# Worker configuration\nworker_class = 'gevent'\nworkers = int(os.environ.get('GUNICORN_WORKERS', multiprocessing.cpu_count() * 2 + 1))\nworker_connections = 1000  # Max simultaneous clients per worker\n\n# HTTP protocols - enable HTTP/2 with HTTP/1.1 fallback\nhttp_protocols = \"h2,h1\"\n\n# SSL/TLS configuration (required for HTTP/2)\n# Default paths work in Docker; override with env vars for local testing\n_default_cert = '/certs/server.crt' if os.path.exists('/certs/server.crt') else 'certs/server.crt'\n_default_key = '/certs/server.key' if os.path.exists('/certs/server.key') else 'certs/server.key'\ncertfile = os.environ.get('GUNICORN_CERTFILE', _default_cert)\nkeyfile = os.environ.get('GUNICORN_KEYFILE', _default_key)\n\n# HTTP/2 specific settings\nhttp2_max_concurrent_streams = 128  # Max streams per connection\nhttp2_initial_window_size = 262144  # 256KB initial flow control window\nhttp2_max_frame_size = 16384  # Default frame size (16KB)\nhttp2_max_header_list_size = 65536  # Max header size\n\n# Timeouts\ntimeout = 30  # Worker timeout\ngraceful_timeout = 30  # Graceful shutdown timeout\nkeepalive = 5  # Keep-alive connections\n\n# Logging\nloglevel = os.environ.get('GUNICORN_LOG_LEVEL', 'info')\naccesslog = '-'  # Log to stdout\nerrorlog = '-'  # Log to stderr\naccess_log_format = '%(h)s %(l)s %(u)s %(t)s \"%(r)s\" %(s)s %(b)s \"%(f)s\" \"%(a)s\" %(L)s'\n\n# Process naming\nproc_name = 'gunicorn-http2-gevent'\n\n# Server mechanics\ndaemon = False\npidfile = None\numask = 0\nuser = None\ngroup = None\ntmp_upload_dir = None\n\n\ndef on_starting(server):\n    \"\"\"Called just before the master process is initialized.\"\"\"\n    server.log.info(\"Starting HTTP/2 server with gevent worker...\")\n    server.log.info(f\"Workers: {workers}, Connections per worker: {worker_connections}\")\n    server.log.info(f\"HTTP/2 max streams: {http2_max_concurrent_streams}\")\n\n\ndef when_ready(server):\n    \"\"\"Called just after the server is started.\"\"\"\n    server.log.info(\"HTTP/2 server is ready to accept connections\")\n\n\ndef worker_int(worker):\n    \"\"\"Called when a worker receives SIGINT or SIGQUIT.\"\"\"\n    worker.log.info(\"Worker received interrupt signal\")\n\n\ndef worker_abort(worker):\n    \"\"\"Called when a worker receives SIGABRT.\"\"\"\n    worker.log.warning(\"Worker aborted\")\n"
  },
  {
    "path": "examples/http2_gevent/test_http2_gevent.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n#!/usr/bin/env python\n\"\"\"\nTests for HTTP/2 with gevent example.\n\nRun with:\n    # Start the server first\n    docker compose up -d\n\n    # Run tests\n    python test_http2_gevent.py\n\n    # Or with pytest\n    pytest test_http2_gevent.py -v\n\nRequirements:\n    pip install httpx[http2] pytest pytest-asyncio\n\"\"\"\n\nimport asyncio\nimport sys\nimport ssl\nimport socket\nimport time\n\n\ndef check_server_available(host='localhost', port=8443, timeout=30):\n    \"\"\"Wait for server to become available.\"\"\"\n    start = time.time()\n    while time.time() - start < timeout:\n        try:\n            ctx = ssl.create_default_context()\n            ctx.check_hostname = False\n            ctx.verify_mode = ssl.CERT_NONE\n            with socket.create_connection((host, port), timeout=2) as sock:\n                with ctx.wrap_socket(sock, server_hostname=host):\n                    return True\n        except (socket.error, ssl.SSLError, OSError):\n            time.sleep(1)\n    return False\n\n\nclass TestHTTP2Gevent:\n    \"\"\"Test HTTP/2 functionality with gevent worker.\"\"\"\n\n    BASE_URL = \"https://localhost:8443\"\n\n    @classmethod\n    def setup_class(cls):\n        \"\"\"Check server is available before running tests.\"\"\"\n        if not check_server_available():\n            raise RuntimeError(\n                \"Server not available. Start it with: docker compose up -d\"\n            )\n\n    def get_client(self):\n        \"\"\"Create HTTP/2 client.\"\"\"\n        import httpx\n        return httpx.Client(http2=True, verify=False, timeout=30.0)\n\n    def test_root_endpoint(self):\n        \"\"\"Test basic GET request returns HTTP/2.\"\"\"\n        with self.get_client() as client:\n            response = client.get(f\"{self.BASE_URL}/\")\n\n            assert response.status_code == 200\n            assert response.http_version == \"HTTP/2\"\n            assert b\"HTTP/2\" in response.content or b\"Gevent\" in response.content\n\n    def test_health_endpoint(self):\n        \"\"\"Test health check endpoint.\"\"\"\n        with self.get_client() as client:\n            response = client.get(f\"{self.BASE_URL}/health\")\n\n            assert response.status_code == 200\n            assert response.text == \"OK\"\n\n    def test_echo_post(self):\n        \"\"\"Test POST echo endpoint.\"\"\"\n        with self.get_client() as client:\n            data = b\"Hello HTTP/2 with Gevent!\"\n            response = client.post(f\"{self.BASE_URL}/echo\", content=data)\n\n            assert response.status_code == 200\n            assert response.content == data\n\n    def test_echo_large_body(self):\n        \"\"\"Test POST with large body (tests flow control).\"\"\"\n        with self.get_client() as client:\n            # 100KB of data\n            data = b\"X\" * (100 * 1024)\n            response = client.post(f\"{self.BASE_URL}/echo\", content=data)\n\n            assert response.status_code == 200\n            assert len(response.content) == len(data)\n            assert response.content == data\n\n    def test_info_endpoint(self):\n        \"\"\"Test JSON info endpoint.\"\"\"\n        with self.get_client() as client:\n            response = client.get(f\"{self.BASE_URL}/info\")\n\n            assert response.status_code == 200\n            info = response.json()\n            assert info['method'] == 'GET'\n            assert info['path'] == '/info'\n            assert 'gevent' in info['server'].lower()\n\n    def test_large_response(self):\n        \"\"\"Test large response (1MB) - tests streaming and flow control.\"\"\"\n        with self.get_client() as client:\n            response = client.get(f\"{self.BASE_URL}/large\")\n\n            assert response.status_code == 200\n            assert len(response.content) == 1024 * 1024\n            assert response.content == b\"X\" * (1024 * 1024)\n\n    def test_streaming_response(self):\n        \"\"\"Test server-sent events style streaming.\"\"\"\n        with self.get_client() as client:\n            response = client.get(f\"{self.BASE_URL}/stream\")\n\n            assert response.status_code == 200\n            assert b\"chunk 0\" in response.content\n            assert b\"chunk 9\" in response.content\n\n    def test_delay_endpoint(self):\n        \"\"\"Test delayed response.\"\"\"\n        with self.get_client() as client:\n            start = time.time()\n            response = client.get(f\"{self.BASE_URL}/delay?seconds=0.5\")\n            elapsed = time.time() - start\n\n            assert response.status_code == 200\n            assert elapsed >= 0.4  # Allow some tolerance\n            assert b\"Delayed\" in response.content\n\n    def test_not_found(self):\n        \"\"\"Test 404 response.\"\"\"\n        with self.get_client() as client:\n            response = client.get(f\"{self.BASE_URL}/nonexistent\")\n\n            assert response.status_code == 404\n\n    def test_gevent_worker_header(self):\n        \"\"\"Test that gevent worker header is present.\"\"\"\n        with self.get_client() as client:\n            response = client.get(f\"{self.BASE_URL}/\")\n\n            assert response.status_code == 200\n            assert response.headers.get('x-worker-type') == 'gevent'\n\n\nclass TestHTTP2Concurrency:\n    \"\"\"Test HTTP/2 multiplexing with concurrent requests.\"\"\"\n\n    BASE_URL = \"https://localhost:8443\"\n\n    @classmethod\n    def setup_class(cls):\n        \"\"\"Check server is available.\"\"\"\n        if not check_server_available():\n            raise RuntimeError(\"Server not available\")\n\n    def test_concurrent_requests_sync(self):\n        \"\"\"Test multiple concurrent requests using threads.\"\"\"\n        import httpx\n        from concurrent.futures import ThreadPoolExecutor, as_completed\n\n        def make_request(i):\n            with httpx.Client(http2=True, verify=False, timeout=30.0) as client:\n                response = client.get(f\"{self.BASE_URL}/delay?seconds=0.2\")\n                return i, response.status_code\n\n        num_requests = 10\n        with ThreadPoolExecutor(max_workers=10) as executor:\n            futures = [executor.submit(make_request, i) for i in range(num_requests)]\n            results = [f.result() for f in as_completed(futures)]\n\n        assert len(results) == num_requests\n        assert all(status == 200 for _, status in results)\n\n\nclass TestHTTP2ConcurrencyAsync:\n    \"\"\"Async tests for HTTP/2 multiplexing.\"\"\"\n\n    BASE_URL = \"https://localhost:8443\"\n\n    @classmethod\n    def setup_class(cls):\n        \"\"\"Check server is available.\"\"\"\n        if not check_server_available():\n            raise RuntimeError(\"Server not available\")\n\n    def test_async_concurrent_requests(self):\n        \"\"\"Test concurrent requests with asyncio.\"\"\"\n        import httpx\n\n        async def run_concurrent():\n            async with httpx.AsyncClient(http2=True, verify=False, timeout=30.0) as client:\n                # Make 10 concurrent requests\n                tasks = [\n                    client.get(f\"{self.BASE_URL}/delay?seconds=0.2\")\n                    for _ in range(10)\n                ]\n                responses = await asyncio.gather(*tasks)\n                return responses\n\n        responses = asyncio.run(run_concurrent())\n\n        assert len(responses) == 10\n        assert all(r.status_code == 200 for r in responses)\n        assert all(r.http_version == \"HTTP/2\" for r in responses)\n\n    def test_async_multiple_streams(self):\n        \"\"\"Test that multiple concurrent streams work over single HTTP/2 connection.\n\n        This test verifies that HTTP/2 can handle multiple concurrent requests,\n        which is the foundation of multiplexing. Performance benefits depend on\n        client library implementation and network conditions.\n        \"\"\"\n        import httpx\n\n        async def run_test():\n            async with httpx.AsyncClient(http2=True, verify=False, timeout=30.0) as client:\n                # Send multiple concurrent requests\n                tasks = [\n                    client.get(f\"{self.BASE_URL}/info\")\n                    for _ in range(10)\n                ]\n                responses = await asyncio.gather(*tasks)\n                return responses\n\n        responses = asyncio.run(run_test())\n\n        # Verify all requests succeeded with HTTP/2\n        assert len(responses) == 10\n        assert all(r.status_code == 200 for r in responses)\n        assert all(r.http_version == \"HTTP/2\" for r in responses)\n\n\ndef run_basic_test():\n    \"\"\"Run a basic test without pytest.\"\"\"\n    print(\"Running basic HTTP/2 gevent test...\")\n\n    if not check_server_available():\n        print(\"ERROR: Server not available at https://localhost:8443\")\n        print(\"Start it with: docker compose up -d\")\n        return False\n\n    try:\n        import httpx\n    except ImportError:\n        print(\"ERROR: httpx not installed. Run: pip install httpx[http2]\")\n        return False\n\n    try:\n        with httpx.Client(http2=True, verify=False, timeout=30.0) as client:\n            # Test basic request\n            print(\"  Testing root endpoint...\", end=\" \")\n            response = client.get(\"https://localhost:8443/\")\n            assert response.status_code == 200\n            assert response.http_version == \"HTTP/2\"\n            print(\"OK\")\n\n            # Test echo\n            print(\"  Testing echo endpoint...\", end=\" \")\n            data = b\"test data\"\n            response = client.post(\"https://localhost:8443/echo\", content=data)\n            assert response.content == data\n            print(\"OK\")\n\n            # Test large response\n            print(\"  Testing large response...\", end=\" \")\n            response = client.get(\"https://localhost:8443/large\")\n            assert len(response.content) == 1024 * 1024\n            print(\"OK\")\n\n            # Test worker header\n            print(\"  Testing gevent worker...\", end=\" \")\n            response = client.get(\"https://localhost:8443/\")\n            assert response.headers.get('x-worker-type') == 'gevent'\n            print(\"OK\")\n\n        print(\"\\nAll basic tests passed!\")\n        return True\n\n    except Exception as e:\n        print(f\"\\nERROR: {e}\")\n        return False\n\n\nif __name__ == '__main__':\n    # Check if pytest is available\n    try:\n        import pytest\n        # Run with pytest if available\n        sys.exit(pytest.main([__file__, '-v']))\n    except ImportError:\n        # Run basic tests without pytest\n        success = run_basic_test()\n        sys.exit(0 if success else 1)\n"
  },
  {
    "path": "examples/log_app.ini",
    "content": "[app:main]\npaste.app_factory = log_app:app_factory\n\n[server:main]\nuse = egg:gunicorn#main\nhost = 127.0.0.1\nport = 8080\nworkers = 3\n\n[loggers]\nkeys=root\n\n[handlers]\nkeys=console\n\n[formatters]\nkeys=default\n\n[logger_root]\nlevel=INFO\nqualname=root\nhandlers=console\n\n[handler_console]\nclass=StreamHandler\nformatter=default\nargs=(sys.stdout, )\n\n[formatter_default]\nformat=[%(asctime)s] [%(levelname)-7s] - %(process)d:%(name)s:%(funcName)s - %(message)s\n"
  },
  {
    "path": "examples/log_app.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport logging\n\nlog = logging.getLogger(__name__)\n\nlog.addHandler(logging.StreamHandler())\n\ndef app_factory(global_options, **local_options):\n    return app\n\ndef app(environ, start_response):\n    start_response(\"200 OK\", [])\n    log.debug(\"Hello Debug!\")\n    log.info(\"Hello Info!\")\n    log.warn(\"Hello Warn!\")\n    log.error(\"Hello Error!\")\n    return [b\"Hello World!\\n\"]\n"
  },
  {
    "path": "examples/logging.conf",
    "content": "[loggers]\nkeys=root, gunicorn.error, gunicorn.access\n\n[handlers]\nkeys=console, error_file, access_file\n\n[formatters]\nkeys=generic, access\n\n[logger_root]\nlevel=INFO\nhandlers=console\n\n[logger_gunicorn.error]\nlevel=INFO\nhandlers=error_file\npropagate=1\nqualname=gunicorn.error\n\n[logger_gunicorn.access]\nlevel=INFO\nhandlers=access_file\npropagate=0\nqualname=gunicorn.access\n\n[handler_console]\nclass=StreamHandler\nformatter=generic\nargs=(sys.stdout, )\n\n[handler_error_file]\nclass=logging.FileHandler\nformatter=generic\nargs=('/tmp/gunicorn.error.log',)\n\n[handler_access_file]\nclass=logging.FileHandler\nformatter=access\nargs=('/tmp/gunicorn.access.log',)\n\n[formatter_generic]\nformat=%(asctime)s [%(process)d] [%(levelname)s] %(message)s\ndatefmt=%Y-%m-%d %H:%M:%S\nclass=logging.Formatter\n\n[formatter_access]\nformat=%(message)s\nclass=logging.Formatter\n"
  },
  {
    "path": "examples/longpoll.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\nimport sys\nimport time\n\nclass TestIter:\n\n    def __iter__(self):\n        lines = [b'line 1\\n', b'line 2\\n']\n        for line in lines:\n            yield line\n            time.sleep(20)\n\ndef app(environ, start_response):\n    \"\"\"Application which cooperatively pauses 20 seconds (needed to surpass normal timeouts) before responding\"\"\"\n    status = '200 OK'\n    response_headers = [\n        ('Content-type', 'text/plain'),\n        ('Transfer-Encoding', \"chunked\"),\n    ]\n    sys.stdout.write('request received')\n    sys.stdout.flush()\n    start_response(status, response_headers)\n    return TestIter()\n"
  },
  {
    "path": "examples/multiapp.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n#\n# Run this application with:\n#\n#   $ gunicorn multiapp:app\n#\n# And then visit:\n#\n#   http://127.0.0.1:8000/app1url\n#   http://127.0.0.1:8000/app2url\n#   http://127.0.0.1:8000/this_is_a_404\n#\n\ntry:\n    from routes import Mapper\nexcept ImportError:\n    print(\"This example requires Routes to be installed\")\n\n# Obviously you'd import your app callables\n# from different places...\nfrom test import app as app1\nfrom test import app as app2\n\n\nclass Application:\n    def __init__(self):\n        self.map = Mapper()\n        self.map.connect('app1', '/app1url', app=app1)\n        self.map.connect('app2', '/app2url', app=app2)\n\n    def __call__(self, environ, start_response):\n        match = self.map.routematch(environ=environ)\n        if not match:\n            return self.error404(environ, start_response)\n        return match[0]['app'](environ, start_response)\n\n    def error404(self, environ, start_response):\n        html = b\"\"\"\\\n        <html>\n          <head>\n            <title>404 - Not Found</title>\n          </head>\n          <body>\n            <h1>404 - Not Found</h1>\n          </body>\n        </html>\n        \"\"\"\n        headers = [\n            ('Content-Type', 'text/html'),\n            ('Content-Length', str(len(html)))\n        ]\n        start_response('404 Not Found', headers)\n        return [html]\n\napp = Application()\n"
  },
  {
    "path": "examples/multidomainapp.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport re\n\nclass SubDomainApp:\n    \"\"\"WSGI application to delegate requests based on domain name.\n\"\"\"\n    def __init__(self, mapping):\n        self.mapping = mapping\n\n    def __call__(self, environ, start_response):\n        host = environ.get(\"HTTP_HOST\", \"\")\n        host = host.split(\":\")[0]  # strip port\n\n        for pattern, app in self.mapping:\n            if re.match(\"^\" + pattern + \"$\", host):\n                return app(environ, start_response)\n        else:\n            start_response(\"404 Not Found\", [])\n            return [b\"\"]\n\ndef hello(environ, start_response):\n    start_response(\"200 OK\", [(\"Content-Type\", \"text/plain\")])\n    return [b\"Hello, world\\n\"]\n\ndef bye(environ, start_response):\n    start_response(\"200 OK\", [(\"Content-Type\", \"text/plain\")])\n    return [b\"Goodbye!\\n\"]\n\napp = SubDomainApp([\n    (\"localhost\", hello),\n    (\".*\", bye)\n])\n"
  },
  {
    "path": "examples/nginx.conf",
    "content": "worker_processes 1;\n\nuser nobody nogroup;\n# 'user nobody nobody;' for systems with 'nobody' as a group instead\nerror_log  /var/log/nginx/error.log warn;\npid /var/run/nginx.pid;\n\nevents {\n  worker_connections 1024; # increase if you have lots of clients\n  accept_mutex off; # set to 'on' if nginx worker_processes > 1\n  # 'use epoll;' to enable for Linux 2.6+\n  # 'use kqueue;' to enable for FreeBSD, OSX\n}\n\nhttp {\n  include mime.types;\n  # fallback in case we can't determine a type\n  default_type application/octet-stream;\n  access_log /var/log/nginx/access.log combined;\n  sendfile on;\n\n  upstream app_server {\n    # fail_timeout=0 means we always retry an upstream even if it failed\n    # to return a good HTTP response\n\n    # for UNIX domain socket setups\n    server unix:/tmp/gunicorn.sock fail_timeout=0;\n\n    # for a TCP configuration\n    # server 192.168.0.7:8000 fail_timeout=0;\n  }\n\n  server {\n    # if no Host match, close the connection to prevent host spoofing\n    listen 80 default_server;\n    return 444;\n  }\n\n  server {\n    # use 'listen 80 deferred;' for Linux\n    # use 'listen 80 accept_filter=httpready;' for FreeBSD\n    listen 80;\n    client_max_body_size 4G;\n\n    # set the correct host(s) for your site\n    server_name example.com www.example.com;\n\n    keepalive_timeout 5;\n\n    # path for static files\n    root /path/to/app/current/public;\n\n    location / {\n      # checks for static file, if not found proxy to app\n      try_files $uri @proxy_to_app;\n    }\n\n    location @proxy_to_app {\n      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n      proxy_set_header X-Forwarded-Proto $scheme;\n      proxy_set_header Host $http_host;\n      # we don't want nginx trying to do something clever with\n      # redirects, we set the Host: header above already.\n      proxy_redirect off;\n      proxy_pass http://app_server;\n    }\n\n    error_page 500 502 503 504 /500.html;\n    location = /500.html {\n      root /path/to/app/current/public;\n    }\n  }\n}\n"
  },
  {
    "path": "examples/read_django_settings.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nUse this config file in your script like this:\n\n    $ gunicorn project_name.wsgi:application -c read_django_settings.py\n\"\"\"\n\nsettings_dict = {}\n\nwith open('frameworks/django/testing/testing/settings.py') as f:\n    exec(f.read(), settings_dict)\n\nloglevel = 'warning'\nproc_name = 'web-project'\nworkers = 1\n\nif settings_dict['DEBUG']:\n    loglevel = 'debug'\n    reload = True\n    proc_name += '_debug'\n"
  },
  {
    "path": "examples/readline_app.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n#\n# Simple example of readline, reading from a stream then echoing the response\n#\n# Usage:\n#\n# Launch a server with the app in a terminal\n#\n#     $ gunicorn -w3 readline_app:app\n#\n# Then in another terminal launch the following command:\n#\n#     $ curl -XPOST -d'test\\r\\ntest2\\r\\n' -H\"Transfer-Encoding: Chunked\" http://localhost:8000\n\n\n\nfrom gunicorn import __version__\n\n\ndef app(environ, start_response):\n    \"\"\"Simplest possible application object\"\"\"\n    status = '200 OK'\n\n    response_headers = [\n        ('Content-type', 'text/plain'),\n        ('Transfer-Encoding', \"chunked\"),\n        ('X-Gunicorn-Version', __version__)\n    ]\n    start_response(status, response_headers)\n\n    body = environ['wsgi.input']\n\n    lines = []\n    while True:\n        line = body.readline()\n        if line == b\"\":\n            break\n        print(line)\n        lines.append(line)\n\n    return iter(lines)\n"
  },
  {
    "path": "examples/sendfile.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n#\n# Example code from Eventlet sources\n\nimport os\nfrom wsgiref.validate import validator\n\n\n# @validator  # breaks sendfile\ndef app(environ, start_response):\n    \"\"\"Simplest possible application object\"\"\"\n    status = '200 OK'\n    fname = os.path.join(os.path.dirname(__file__), \"hello.txt\")\n    f = open(fname, 'rb')\n\n    response_headers = [\n        ('Content-type', 'text/plain'),\n    ]\n    start_response(status, response_headers)\n\n    return environ['wsgi.file_wrapper'](f)\n"
  },
  {
    "path": "examples/server.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDdDCCAlwCCQC3MfdcOMwt6DANBgkqhkiG9w0BAQUFADB8MQswCQYDVQQGEwJG\nUjERMA8GA1UECBMIUGljYXJkaWUxDjAMBgNVBAcTBUNyZWlsMREwDwYDVQQKEwhn\ndW5pY29ybjEVMBMGA1UEAxMMZ3VuaWNvcm4ub3JnMSAwHgYJKoZIhvcNAQkBFhF1\nc2VyQGd1bmljb3JuLm9yZzAeFw0xMjEyMTQwODI2MDJaFw0xMzEyMTQwODI2MDJa\nMHwxCzAJBgNVBAYTAkZSMREwDwYDVQQIEwhQaWNhcmRpZTEOMAwGA1UEBxMFQ3Jl\naWwxETAPBgNVBAoTCGd1bmljb3JuMRUwEwYDVQQDEwxndW5pY29ybi5vcmcxIDAe\nBgkqhkiG9w0BCQEWEXVzZXJAZ3VuaWNvcm4ub3JnMIIBIjANBgkqhkiG9w0BAQEF\nAAOCAQ8AMIIBCgKCAQEAy9RQSiGpB+HyjMpRCEfV9M/4g7gXq/qRizxDspJujoBz\nSW0d4FqMHaSRX2QOA+euhtlOYTgsvWZcyv5cvDfL1CtrNWSVBrlo7wIy5tg60Z3A\nJnWT/Zxj4WIqkPwdglB1sRBsI1Fn0o6nJu4HekZedXDK6fua4lOPfsQG84EhRQKS\nMz2o7Nesk8/UMjb+5WoRmG7mxrpe0/OYlnydqzqwHUQ+I5CHl1kOhePo9ZBTFMA5\nEce8kGQs37rFCEy92xCYHDgp+CjjyYbeBskF3o0/a88K2bt8J7uXkn4h14HjtFHq\nfYnqn60cwyIx3T/uMUh6EmhKQezaw60xyIivmjH8tQIDAQABMA0GCSqGSIb3DQEB\nBQUAA4IBAQAKu7kzTAqONFI1qC6mnwAixSd7ml6RtyQRiIWjg4FyTJmS2NMlqUSI\nCiV1g1+pv2cy9amld8hoO17ISYFZqMoRxJgD5GuN4y1lUefFe95GHI9loubIJZlR\n5KlZEvCiaAQoGvYiacf4BNkljyrwgPVM5e71dGon7jyghmV6yUaUL6+1J8BU/KYg\njz8RtMtptqkwKPKQVfuDcr/eoH6uZwPRbyfqSui8SuMz3Df6Dnx1hOtlQRJC6eNo\nU9L3jkmCsbbMNBAz6iQjyFHFa9iqzwL7nvqZTryjmI5Dpn+BnT7Q5cduK+N5vt4+\nRjNVrz/l6+nR68B5GO96zUTV3/KrEmFr\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "examples/server.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEAy9RQSiGpB+HyjMpRCEfV9M/4g7gXq/qRizxDspJujoBzSW0d\n4FqMHaSRX2QOA+euhtlOYTgsvWZcyv5cvDfL1CtrNWSVBrlo7wIy5tg60Z3AJnWT\n/Zxj4WIqkPwdglB1sRBsI1Fn0o6nJu4HekZedXDK6fua4lOPfsQG84EhRQKSMz2o\n7Nesk8/UMjb+5WoRmG7mxrpe0/OYlnydqzqwHUQ+I5CHl1kOhePo9ZBTFMA5Ece8\nkGQs37rFCEy92xCYHDgp+CjjyYbeBskF3o0/a88K2bt8J7uXkn4h14HjtFHqfYnq\nn60cwyIx3T/uMUh6EmhKQezaw60xyIivmjH8tQIDAQABAoIBAQDFzhTc3C2daLhp\nyS06S/xmyCz0JwNR8qir5qAL++8ue5ll+G61+yle2wX4/LBdOck1NE3MKye/5kbG\n+HImdj9od3pjJmk5TVV4HToorE7ofZ6rtA8aX1rOruWALiq0/EA6xSUsYSPQQoAU\nV4sKLqAceIly6Kk2WsE21CWqyfXvcQOtfBYmFwmPCImWecJLypeheEpz1U2EYl65\nu6b0NsXeODrLXEAEFjdb6UBJtzRtTJ/OnbDvghu9xMjlT0Pj+inoAv/ePZB8bmvH\nXGhZo7dzgsDZ+eys7XnbeggUOhFImzCjO1f3pIVXWThGDgKIrpc9Evac2Q3AjTFY\nNV9HBV9BAoGBAOyWq7HDgeCEu54orPAmdkO4j/HFX+262BTQoVCg4OX3Iv9A/lH4\nlpVGrFlK0qJF9jb7mjDmXP2LW0fwzyHe42DGFbZkKdfiMBuE+qoPeAV9s+SjE4H3\nl3tHoAOFUt2wITcHK4EYjoLMAgrbRNiv9t/gqiMm1oIb3fkUbpOoGG3LAoGBANyN\nkLop3JfN1Kzto7gJq/tLS21joexTU+s4EJ+a4Q8KH1d47DLI+4119Ot+NWwi9X3S\nsbOKZOjfrGw9+HPI64i7hD9HPSK58IUbsfVR9vPlPei45inRfi6s7+EUzKifOKZU\no1ecpOSPYQHZtDToGcQCTS0IFwMXHgrkP380We9/AoGBAI1wljyz8RVUxQWMs7bu\nh4187TFRGkR5i20GPSqCw3E4CkgnhuNihkO/+JF5VeuFf+jnCgtp7PX3Nh8QLATH\nx4+3XIup3goeQzxwh5rbnJlLyRxLEgKFDp6490SjlCLMhU7sjmmjUK+JXz82TzZs\nHF9DZPOW6G7oUg/y0xibSd95AoGAXpDEcU3pq50xh0QNYqei+gh6uthxYScJYF2V\noxmBTjWE4riSbeQHF8xvy1k+BrOmluB0GQtJ4R+minK3yM1pUCM2vPsKl40qN6h8\nUTdnr4OnW9WLunp8o/66i8OjTNmYLJk1wCcF/IoNigGSZuztv0FNXfWOCGEtHHZp\nU11bAnkCgYEAoU0sdFL3IfmxnNQ9CDmgXdJM0SpUm4ECd2jM/fRdgLelL+WislCF\ngHjZw+3mplIzqQ9DMwRkjbaIxP0H92OopOBIqmShWUuzWw/Dj0L8PGe/7skcwsGD\n/VLEkGzrxJwP4kokUu1kvLOqHM429JXsb8wO16iMQAB93yUZ+X8PGfQ=\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "examples/slowclient.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport sys\nimport time\n\n\ndef app(environ, start_response):\n    \"\"\"Application which cooperatively pauses 10 seconds before responding\"\"\"\n    data = b'Hello, World!\\n'\n    status = '200 OK'\n    response_headers = [\n        ('Content-type', 'text/plain'),\n        ('Content-Length', str(len(data))),\n    ]\n    sys.stdout.write('request received, pausing 10 seconds')\n    sys.stdout.flush()\n    time.sleep(10)\n    start_response(status, response_headers)\n    return iter([data])\n"
  },
  {
    "path": "examples/standalone_app.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n#!/usr/bin/env python\n#\n# An example of a standalone application using the internal API of Gunicorn.\n#\n#   $ python standalone_app.py\n#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport multiprocessing\n\nimport gunicorn.app.base\n\n\ndef number_of_workers():\n    return (multiprocessing.cpu_count() * 2) + 1\n\n\ndef handler_app(environ, start_response):\n    response_body = b'Works fine'\n    status = '200 OK'\n\n    response_headers = [\n        ('Content-Type', 'text/plain'),\n    ]\n\n    start_response(status, response_headers)\n\n    return [response_body]\n\n\nclass StandaloneApplication(gunicorn.app.base.BaseApplication):\n\n    def __init__(self, app, options=None):\n        self.options = options or {}\n        self.application = app\n        super().__init__()\n\n    def load_config(self):\n        config = {key: value for key, value in self.options.items()\n                  if key in self.cfg.settings and value is not None}\n        for key, value in config.items():\n            self.cfg.set(key.lower(), value)\n\n    def load(self):\n        return self.application\n\n\nif __name__ == '__main__':\n    options = {\n        'bind': '%s:%s' % ('127.0.0.1', '8080'),\n        'workers': number_of_workers(),\n    }\n    StandaloneApplication(handler_app, options).run()\n"
  },
  {
    "path": "examples/streaming_chat/Dockerfile",
    "content": "FROM python:3.12-slim\n\nWORKDIR /app\n\n# Install dependencies\nRUN pip install --no-cache-dir \\\n    fastapi \\\n    pydantic\n\n# Copy gunicorn source\nCOPY . /app/gunicorn-src\nRUN pip install /app/gunicorn-src\n\n# Copy app\nCOPY examples/streaming_chat /app/streaming_chat\n\nENV PYTHONPATH=/app\n\nEXPOSE 8000\nCMD [\"gunicorn\", \"streaming_chat.main:app\", \"-c\", \"streaming_chat/gunicorn_conf.py\"]\n"
  },
  {
    "path": "examples/streaming_chat/README.md",
    "content": "# Streaming Chat Example\n\nA FastAPI-based chat demo that simulates LLM token-by-token streaming, powered\nby Gunicorn's dirty workers for efficient long-running operations.\n\n## Overview\n\nThis example demonstrates how to build a streaming chat API that:\n- Streams tokens word-by-word like ChatGPT (Server-Sent Events)\n- Uses dirty workers for the \"inference\" workload\n- Includes a browser-based chat UI for testing\n- Requires no ML dependencies (simulated responses)\n\n## Architecture\n\n```\n┌─────────────────┐     ┌──────────────────┐     ┌─────────────────────┐\n│  Browser/curl   │────►│  FastAPI (ASGI)  │────►│  DirtyWorker        │\n│  SSE stream     │     │  - /chat (SSE)   │     │  - ChatApp          │\n│                 │◄────│  - /chat/sync    │◄────│  - Token generator  │\n└─────────────────┘     └──────────────────┘     └─────────────────────┘\n                              │\n                              ▼\n                        text/event-stream\n                        data: {\"token\": \"Hello\"}\n                        data: {\"token\": \" \"}\n                        data: {\"token\": \"world\"}\n                        data: [DONE]\n```\n\n**Why streaming with dirty workers?**\n- Real LLM inference is slow (seconds to minutes)\n- Users expect to see responses appear gradually\n- Dirty workers keep the \"model\" loaded between requests\n- HTTP workers remain responsive during streaming\n\n## Quick Start\n\n### With Docker (recommended)\n\n```bash\ncd examples/streaming_chat\ndocker compose up --build\n```\n\nThen open http://localhost:8000 in your browser.\n\n### Local Development\n\n```bash\n# Install dependencies\npip install fastapi pydantic\n\n# Run with gunicorn\ngunicorn examples.streaming_chat.main:app \\\n  -c examples/streaming_chat/gunicorn_conf.py\n```\n\n## API Reference\n\n### POST /chat\n\nStream a chat response using Server-Sent Events.\n\n**Request:**\n```json\n{\n  \"prompt\": \"hello\",\n  \"thinking\": false\n}\n```\n\n**Response:** `text/event-stream`\n```\ndata: {\"token\": \"Hello\"}\n\ndata: {\"token\": \"!\"}\n\ndata: {\"token\": \" \"}\n\ndata: {\"token\": \"I'm\"}\n\n...\n\ndata: [DONE]\n```\n\n**Example with curl:**\n```bash\ncurl -N http://localhost:8000/chat \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"prompt\": \"hello\"}'\n```\n\n### POST /chat/sync\n\nNon-streaming version that returns the complete response.\n\n**Request:**\n```json\n{\n  \"prompt\": \"hello\"\n}\n```\n\n**Response:**\n```json\n{\n  \"response\": \"Hello! I'm a simulated AI assistant...\"\n}\n```\n\n### GET /health\n\nHealth check endpoint.\n\n**Response:**\n```json\n{\"status\": \"ok\"}\n```\n\n### GET /\n\nBrowser-based chat UI for testing.\n\n## Configuration\n\nEdit `gunicorn_conf.py` to adjust:\n\n| Setting | Default | Description |\n|---------|---------|-------------|\n| `workers` | 2 | Number of HTTP workers |\n| `dirty_workers` | 1 | Number of dirty workers |\n| `dirty_timeout` | 60 | Max seconds per request |\n| `bind` | 0.0.0.0:8000 | Listen address |\n\n## Prompts\n\nThe simulated chat app responds to these keywords:\n\n| Keyword | Response |\n|---------|----------|\n| `hello`, `hi`, `hey` | Greeting message |\n| `explain` | Explanation of dirty workers |\n| `streaming` | How streaming works |\n| `code` | Example code snippet |\n| (default) | Generic thoughtful response |\n\n## Features Demonstrated\n\n1. **Token streaming** - Word-by-word output via generators\n2. **SSE protocol** - Browser-compatible event streaming\n3. **Async generators** - Using `stream_async()` from dirty client\n4. **Thinking mode** - Multi-phase streaming with visible \"thinking\"\n5. **Browser UI** - Interactive chat with cursor animation\n\n## Testing\n\nRun the integration tests:\n\n```bash\n# Start the service first\ndocker compose up -d\n\n# Run tests\npip install requests\npython test_streaming.py\n```\n\n## Adapting for Real LLMs\n\nTo use a real LLM instead of simulated responses:\n\n```python\n# chat_app.py\nfrom gunicorn.dirty.app import DirtyApp\n\nclass ChatApp(DirtyApp):\n    def init(self):\n        from transformers import pipeline\n        self.generator = pipeline(\"text-generation\", model=\"gpt2\")\n\n    def generate(self, prompt):\n        for output in self.generator(prompt, max_new_tokens=100, do_sample=True):\n            # Yield tokens as they're generated\n            yield output[\"generated_text\"]\n\n    def close(self):\n        del self.generator\n```\n\nOr with an API-based LLM:\n\n```python\nclass ChatApp(DirtyApp):\n    def init(self):\n        import openai\n        self.client = openai.OpenAI()\n\n    async def generate(self, prompt):\n        stream = self.client.chat.completions.create(\n            model=\"gpt-4\",\n            messages=[{\"role\": \"user\", \"content\": prompt}],\n            stream=True\n        )\n        for chunk in stream:\n            if chunk.choices[0].delta.content:\n                yield chunk.choices[0].delta.content\n```\n\n## Production Considerations\n\n1. **Real LLM**: Replace `ChatApp` with actual model inference\n2. **GPU Support**: Add CUDA to Dockerfile for faster inference\n3. **Rate Limiting**: Add FastAPI middleware for rate limiting\n4. **Authentication**: Add API key validation\n5. **Monitoring**: Add Prometheus metrics endpoint\n6. **Timeouts**: Adjust `dirty_timeout` based on max response length\n"
  },
  {
    "path": "examples/streaming_chat/__init__.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n# Streaming Chat Example\n# Demonstrates dirty worker streaming with simulated LLM token generation\n"
  },
  {
    "path": "examples/streaming_chat/chat_app.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport time\nimport random\nfrom gunicorn.dirty.app import DirtyApp\n\n\nclass ChatApp(DirtyApp):\n    \"\"\"Simulated LLM chat application demonstrating streaming responses.\n\n    This app mimics LLM token-by-token generation without requiring\n    heavy ML dependencies. Each response is streamed word-by-word\n    with realistic timing delays.\n    \"\"\"\n\n    def init(self):\n        \"\"\"Initialize canned responses for different prompts.\"\"\"\n        self.responses = {\n            \"hello\": (\n                \"Hello! I'm a simulated AI assistant running on Gunicorn's \"\n                \"dirty workers. I can demonstrate streaming responses just \"\n                \"like a real LLM, but without the heavy ML dependencies. \"\n                \"How can I help you today?\"\n            ),\n            \"explain\": (\n                \"Dirty workers are separate processes that handle long-running \"\n                \"tasks like ML inference. They keep models loaded in memory \"\n                \"across requests, avoiding expensive reload times. HTTP workers \"\n                \"remain lightweight and responsive while dirty workers handle \"\n                \"the heavy computation. This architecture is inspired by \"\n                \"Erlang's dirty schedulers.\"\n            ),\n            \"streaming\": (\n                \"Streaming works by yielding chunks from a generator function. \"\n                \"Each yield sends a chunk message through the IPC socket. The \"\n                \"client receives chunks as they're produced, enabling real-time \"\n                \"token-by-token display. This is perfect for LLM applications \"\n                \"where users expect to see responses appear gradually.\"\n            ),\n            \"code\": (\n                \"Here's a simple example:\\n\\n\"\n                \"```python\\n\"\n                \"from gunicorn.dirty import get_dirty_client\\n\\n\"\n                \"client = get_dirty_client()\\n\"\n                \"for token in client.stream('app:ChatApp', 'generate', prompt):\\n\"\n                \"    print(token, end='', flush=True)\\n\"\n                \"```\\n\\n\"\n                \"This streams tokens directly to the console as they arrive.\"\n            ),\n            \"default\": (\n                \"I understand your question. Let me think about that for a \"\n                \"moment. The key insight here is that streaming responses \"\n                \"provide a much better user experience for long-running \"\n                \"operations. Instead of waiting for the complete response, \"\n                \"users see content appearing in real-time, which feels more \"\n                \"interactive and responsive.\"\n            ),\n        }\n        self.min_delay = 0.03  # Minimum delay between tokens (30ms)\n        self.max_delay = 0.08  # Maximum delay between tokens (80ms)\n\n    def generate(self, prompt):\n        \"\"\"Generate a streaming response for the given prompt.\n\n        Yields tokens (words) one at a time with realistic delays\n        to simulate LLM inference.\n\n        Args:\n            prompt: User's input prompt\n\n        Yields:\n            str: Individual tokens (words with trailing space)\n        \"\"\"\n        response = self._get_response(prompt)\n        words = response.split()\n\n        for i, word in enumerate(words):\n            # Simulate variable inference time\n            delay = random.uniform(self.min_delay, self.max_delay)\n            time.sleep(delay)\n\n            # Add space after word (except last word)\n            if i < len(words) - 1:\n                yield word + \" \"\n            else:\n                yield word\n\n    def generate_with_thinking(self, prompt):\n        \"\"\"Generate response with visible 'thinking' phase.\n\n        First yields thinking indicators, then streams the response.\n        Demonstrates multi-phase streaming.\n\n        Args:\n            prompt: User's input prompt\n\n        Yields:\n            str: Thinking indicators followed by response tokens\n        \"\"\"\n        # Thinking phase\n        yield \"[thinking\"\n        for _ in range(3):\n            time.sleep(0.3)\n            yield \".\"\n        yield \"]\\n\\n\"\n\n        # Response phase\n        yield from self.generate(prompt)\n\n    def _get_response(self, prompt):\n        \"\"\"Match prompt to a canned response.\n\n        Args:\n            prompt: User's input prompt\n\n        Returns:\n            str: Matched response text\n        \"\"\"\n        prompt_lower = prompt.lower().strip()\n\n        # Check for keyword matches\n        for key, response in self.responses.items():\n            if key in prompt_lower:\n                return response\n\n        # Greeting patterns\n        if any(g in prompt_lower for g in [\"hi\", \"hey\", \"greetings\"]):\n            return self.responses[\"hello\"]\n\n        return self.responses[\"default\"]\n\n    def close(self):\n        \"\"\"Cleanup on shutdown.\"\"\"\n        pass\n"
  },
  {
    "path": "examples/streaming_chat/demo_capture.txt",
    "content": "================================================================================\n                        STREAMING CHAT DEMO CAPTURE\n                     Gunicorn Dirty Workers + FastAPI SSE\n================================================================================\n\n$ curl -s http://127.0.0.1:8000/health\n{\"status\":\"ok\"}\n\n================================================================================\n                           TEST 1: Hello Prompt\n================================================================================\n\n$ curl -N http://127.0.0.1:8000/chat -d '{\"prompt\": \"hello\"}'\n\ndata: {\"token\": \"Hello! \"}\n\ndata: {\"token\": \"I'm \"}\n\ndata: {\"token\": \"a \"}\n\ndata: {\"token\": \"simulated \"}\n\ndata: {\"token\": \"AI \"}\n\ndata: {\"token\": \"assistant \"}\n\ndata: {\"token\": \"running \"}\n\ndata: {\"token\": \"on \"}\n\ndata: {\"token\": \"Gunicorn's \"}\n\ndata: {\"token\": \"dirty \"}\n\ndata: {\"token\": \"workers. \"}\n\ndata: {\"token\": \"I \"}\n\ndata: {\"token\": \"can \"}\n\ndata: {\"token\": \"demonstrate \"}\n\ndata: {\"token\": \"streaming \"}\n\ndata: {\"token\": \"responses \"}\n\ndata: {\"token\": \"just \"}\n\ndata: {\"token\": \"like \"}\n\ndata: {\"token\": \"a \"}\n\ndata: {\"token\": \"real \"}\n\ndata: {\"token\": \"LLM, \"}\n\ndata: {\"token\": \"but \"}\n\ndata: {\"token\": \"without \"}\n\ndata: {\"token\": \"the \"}\n\ndata: {\"token\": \"heavy \"}\n\ndata: {\"token\": \"ML \"}\n\ndata: {\"token\": \"dependencies. \"}\n\ndata: {\"token\": \"How \"}\n\ndata: {\"token\": \"can \"}\n\ndata: {\"token\": \"I \"}\n\ndata: {\"token\": \"help \"}\n\ndata: {\"token\": \"you \"}\n\ndata: {\"token\": \"today?\"}\n\ndata: [DONE]\n\n================================================================================\n                        TEST 2: Explain Dirty Workers\n================================================================================\n\n$ curl -N http://127.0.0.1:8000/chat -d '{\"prompt\": \"explain dirty workers\"}'\n\ndata: {\"token\": \"Dirty \"}\n\ndata: {\"token\": \"workers \"}\n\ndata: {\"token\": \"are \"}\n\ndata: {\"token\": \"separate \"}\n\ndata: {\"token\": \"processes \"}\n\ndata: {\"token\": \"that \"}\n\ndata: {\"token\": \"handle \"}\n\ndata: {\"token\": \"long-running \"}\n\ndata: {\"token\": \"tasks \"}\n\ndata: {\"token\": \"like \"}\n\ndata: {\"token\": \"ML \"}\n\ndata: {\"token\": \"inference. \"}\n\ndata: {\"token\": \"They \"}\n\ndata: {\"token\": \"keep \"}\n\ndata: {\"token\": \"models \"}\n\ndata: {\"token\": \"loaded \"}\n\ndata: {\"token\": \"in \"}\n\ndata: {\"token\": \"memory \"}\n\ndata: {\"token\": \"across \"}\n\ndata: {\"token\": \"requests, \"}\n\ndata: {\"token\": \"avoiding \"}\n\ndata: {\"token\": \"expensive \"}\n\ndata: {\"token\": \"reload \"}\n\ndata: {\"token\": \"times. \"}\n\ndata: {\"token\": \"HTTP \"}\n\ndata: {\"token\": \"workers \"}\n\ndata: {\"token\": \"remain \"}\n\ndata: {\"token\": \"lightweight \"}\n\ndata: {\"token\": \"and \"}\n\ndata: {\"token\": \"responsive \"}\n\ndata: {\"token\": \"while \"}\n\ndata: {\"token\": \"dirty \"}\n\ndata: {\"token\": \"workers \"}\n\ndata: {\"token\": \"handle \"}\n\ndata: {\"token\": \"the \"}\n\ndata: {\"token\": \"heavy \"}\n\ndata: {\"token\": \"computation. \"}\n\ndata: {\"token\": \"This \"}\n\ndata: {\"token\": \"architecture \"}\n\ndata: {\"token\": \"is \"}\n\ndata: {\"token\": \"inspired \"}\n\ndata: {\"token\": \"by \"}\n\ndata: {\"token\": \"Erlang's \"}\n\ndata: {\"token\": \"dirty \"}\n\ndata: {\"token\": \"schedulers.\"}\n\ndata: [DONE]\n\n================================================================================\n                         TEST 3: Sync Endpoint\n================================================================================\n\n$ curl -s http://127.0.0.1:8000/chat/sync -d '{\"prompt\": \"hello\"}'\n\n{\"response\":\"Hello! I'm a simulated AI assistant running on Gunicorn's dirty workers. I can demonstrate streaming responses just like a real LLM, but without the heavy ML dependencies. How can I help you today?\"}\n\n================================================================================\n                         DEMO COMPLETE\n================================================================================\n\nBrowser UI available at: http://localhost:8000/\n\nFeatures demonstrated:\n  - Token-by-token SSE streaming\n  - Async generators via dirty workers\n  - Different responses based on keywords\n  - Sync endpoint for comparison\n  - Health check endpoint\n\nServer Logs:\n[INFO] Starting gunicorn 24.1.0\n[INFO] Listening at: http://0.0.0.0:8000 (1)\n[INFO] Using worker: asgi\n[INFO] Spawned dirty arbiter (pid: 7)\n[INFO] Dirty arbiter starting (pid: 7)\n[INFO] Booting worker with pid: 8\n[INFO] Dirty arbiter listening on /tmp/gunicorn-dirty-.../arbiter.sock\n[INFO] Spawned dirty worker (pid: 9)\n[INFO] Initialized dirty app: streaming_chat.chat_app:ChatApp\n[INFO] Dirty worker 9 listening on /tmp/gunicorn-dirty-.../worker-1.sock\n[INFO] ASGI server listening on http://0.0.0.0:8000\n"
  },
  {
    "path": "examples/streaming_chat/docker-compose.yml",
    "content": "services:\n  streaming-chat:\n    build:\n      context: ../..\n      dockerfile: examples/streaming_chat/Dockerfile\n    ports:\n      - \"8000:8000\"\n    healthcheck:\n      test: [\"CMD\", \"python\", \"-c\", \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=5)\"]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n      start_period: 5s\n"
  },
  {
    "path": "examples/streaming_chat/gunicorn_conf.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nbind = \"0.0.0.0:8000\"\nworkers = 2\nworker_class = \"asgi\"\n\n# Dirty worker config\ndirty_apps = [\"streaming_chat.chat_app:ChatApp\"]\ndirty_workers = 1\ndirty_timeout = 60\n"
  },
  {
    "path": "examples/streaming_chat/main.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport json\nfrom fastapi import FastAPI\nfrom fastapi.responses import StreamingResponse, HTMLResponse\nfrom pydantic import BaseModel\nfrom gunicorn.dirty.client import get_dirty_client_async\n\n\napp = FastAPI(\n    title=\"Streaming Chat Demo\",\n    description=\"Demonstrates dirty worker streaming with simulated LLM responses\",\n)\n\n\nclass ChatRequest(BaseModel):\n    prompt: str\n    thinking: bool = False\n\n\nclass ChatResponse(BaseModel):\n    response: str\n\n\n@app.post(\"/chat\")\nasync def chat(request: ChatRequest):\n    \"\"\"Stream a chat response using Server-Sent Events.\n\n    The response is streamed token-by-token, simulating LLM inference.\n    Each token is sent as an SSE event with JSON data.\n\n    Args:\n        request: Chat request with prompt and optional thinking mode\n\n    Returns:\n        StreamingResponse with text/event-stream content type\n    \"\"\"\n    client = await get_dirty_client_async()\n    action = \"generate_with_thinking\" if request.thinking else \"generate\"\n\n    async def stream():\n        async for token in client.stream_async(\n            \"streaming_chat.chat_app:ChatApp\",\n            action,\n            request.prompt\n        ):\n            data = json.dumps({\"token\": token})\n            yield f\"data: {data}\\n\\n\"\n        yield \"data: [DONE]\\n\\n\"\n\n    return StreamingResponse(\n        stream(),\n        media_type=\"text/event-stream\",\n        headers={\n            \"Cache-Control\": \"no-cache\",\n            \"Connection\": \"keep-alive\",\n            \"X-Accel-Buffering\": \"no\",  # Disable nginx buffering\n        }\n    )\n\n\n@app.post(\"/chat/sync\", response_model=ChatResponse)\nasync def chat_sync(request: ChatRequest):\n    \"\"\"Non-streaming chat endpoint for comparison.\n\n    Waits for the complete response before returning.\n    Useful for testing or when streaming isn't needed.\n\n    Args:\n        request: Chat request with prompt\n\n    Returns:\n        Complete response as JSON\n    \"\"\"\n    client = await get_dirty_client_async()\n    action = \"generate_with_thinking\" if request.thinking else \"generate\"\n\n    tokens = []\n    async for token in client.stream_async(\n        \"streaming_chat.chat_app:ChatApp\",\n        action,\n        request.prompt\n    ):\n        tokens.append(token)\n\n    return ChatResponse(response=\"\".join(tokens))\n\n\n@app.get(\"/health\")\nasync def health():\n    \"\"\"Health check endpoint.\"\"\"\n    return {\"status\": \"ok\"}\n\n\n@app.get(\"/\", response_class=HTMLResponse)\nasync def index():\n    \"\"\"Simple chat UI for testing streaming.\"\"\"\n    return \"\"\"\n<!DOCTYPE html>\n<html>\n<head>\n    <title>Streaming Chat Demo</title>\n    <style>\n        body {\n            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n            max-width: 800px;\n            margin: 0 auto;\n            padding: 20px;\n            background: #1a1a2e;\n            color: #eee;\n        }\n        h1 { color: #00d9ff; }\n        .chat-container {\n            background: #16213e;\n            border-radius: 8px;\n            padding: 20px;\n            margin: 20px 0;\n        }\n        #response {\n            min-height: 100px;\n            padding: 15px;\n            background: #0f0f23;\n            border-radius: 4px;\n            white-space: pre-wrap;\n            font-family: 'Monaco', 'Menlo', monospace;\n            line-height: 1.6;\n        }\n        .input-group {\n            display: flex;\n            gap: 10px;\n            margin-top: 15px;\n        }\n        input[type=\"text\"] {\n            flex: 1;\n            padding: 12px;\n            border: 1px solid #333;\n            border-radius: 4px;\n            background: #0f0f23;\n            color: #eee;\n            font-size: 16px;\n        }\n        button {\n            padding: 12px 24px;\n            background: #00d9ff;\n            color: #000;\n            border: none;\n            border-radius: 4px;\n            cursor: pointer;\n            font-weight: bold;\n        }\n        button:hover { background: #00b8d9; }\n        button:disabled { background: #555; cursor: not-allowed; }\n        .checkbox-group {\n            margin-top: 10px;\n        }\n        label { cursor: pointer; }\n        .suggestions {\n            margin-top: 15px;\n            display: flex;\n            flex-wrap: wrap;\n            gap: 8px;\n        }\n        .suggestion {\n            padding: 6px 12px;\n            background: #333;\n            border-radius: 4px;\n            cursor: pointer;\n            font-size: 14px;\n        }\n        .suggestion:hover { background: #444; }\n        .cursor {\n            display: inline-block;\n            width: 8px;\n            height: 18px;\n            background: #00d9ff;\n            animation: blink 1s infinite;\n            vertical-align: text-bottom;\n        }\n        @keyframes blink {\n            0%, 50% { opacity: 1; }\n            51%, 100% { opacity: 0; }\n        }\n    </style>\n</head>\n<body>\n    <h1>Streaming Chat Demo</h1>\n    <p>This demo shows token-by-token streaming using Gunicorn's dirty workers.</p>\n\n    <div class=\"chat-container\">\n        <div id=\"response\"></div>\n        <div class=\"input-group\">\n            <input type=\"text\" id=\"prompt\" placeholder=\"Type a message...\"\n                   onkeypress=\"if(event.key==='Enter') sendMessage()\">\n            <button onclick=\"sendMessage()\" id=\"sendBtn\">Send</button>\n        </div>\n        <div class=\"checkbox-group\">\n            <label>\n                <input type=\"checkbox\" id=\"thinking\"> Show thinking phase\n            </label>\n        </div>\n        <div class=\"suggestions\">\n            <span class=\"suggestion\" onclick=\"setPrompt('hello')\">hello</span>\n            <span class=\"suggestion\" onclick=\"setPrompt('explain dirty workers')\">explain</span>\n            <span class=\"suggestion\" onclick=\"setPrompt('how does streaming work?')\">streaming</span>\n            <span class=\"suggestion\" onclick=\"setPrompt('show me code')\">code</span>\n        </div>\n    </div>\n\n    <script>\n        function setPrompt(text) {\n            document.getElementById('prompt').value = text;\n            sendMessage();\n        }\n\n        async function sendMessage() {\n            const promptEl = document.getElementById('prompt');\n            const responseEl = document.getElementById('response');\n            const sendBtn = document.getElementById('sendBtn');\n            const thinking = document.getElementById('thinking').checked;\n\n            const prompt = promptEl.value.trim();\n            if (!prompt) return;\n\n            sendBtn.disabled = true;\n            responseEl.innerHTML = '<span class=\"cursor\"></span>';\n\n            try {\n                const response = await fetch('/chat', {\n                    method: 'POST',\n                    headers: {'Content-Type': 'application/json'},\n                    body: JSON.stringify({prompt, thinking})\n                });\n\n                const reader = response.body.getReader();\n                const decoder = new TextDecoder();\n                let text = '';\n\n                while (true) {\n                    const {done, value} = await reader.read();\n                    if (done) break;\n\n                    const chunk = decoder.decode(value);\n                    const lines = chunk.split('\\\\n');\n\n                    for (const line of lines) {\n                        if (line.startsWith('data: ')) {\n                            const data = line.slice(6);\n                            if (data === '[DONE]') {\n                                responseEl.textContent = text;\n                            } else {\n                                try {\n                                    const parsed = JSON.parse(data);\n                                    text += parsed.token;\n                                    responseEl.innerHTML = text + '<span class=\"cursor\"></span>';\n                                } catch (e) {}\n                            }\n                        }\n                    }\n                }\n            } catch (error) {\n                responseEl.textContent = 'Error: ' + error.message;\n            }\n\n            sendBtn.disabled = false;\n            promptEl.value = '';\n            promptEl.focus();\n        }\n\n        document.getElementById('prompt').focus();\n    </script>\n</body>\n</html>\n\"\"\"\n"
  },
  {
    "path": "examples/streaming_chat/requirements.txt",
    "content": "fastapi>=0.100.0\npydantic>=2.0.0\n"
  },
  {
    "path": "examples/streaming_chat/test_streaming.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Integration tests for the streaming chat example.\"\"\"\n\nimport json\nimport os\nimport requests\n\n\ndef test_health_endpoint():\n    \"\"\"Test the health check endpoint.\"\"\"\n    base_url = os.environ.get(\"STREAMING_CHAT_URL\", \"http://127.0.0.1:8000\")\n    response = requests.get(f\"{base_url}/health\")\n    assert response.status_code == 200\n    assert response.json() == {\"status\": \"ok\"}\n    print(\"Health check: OK\")\n\n\ndef test_streaming_chat():\n    \"\"\"Test that chat endpoint streams tokens via SSE.\"\"\"\n    base_url = os.environ.get(\"STREAMING_CHAT_URL\", \"http://127.0.0.1:8000\")\n\n    response = requests.post(\n        f\"{base_url}/chat\",\n        json={\"prompt\": \"hello\"},\n        stream=True,\n        headers={\"Accept\": \"text/event-stream\"}\n    )\n    assert response.status_code == 200\n    assert response.headers.get(\"content-type\") == \"text/event-stream; charset=utf-8\"\n\n    tokens = []\n    for line in response.iter_lines(decode_unicode=True):\n        if line.startswith(\"data: \"):\n            data = line[6:]\n            if data == \"[DONE]\":\n                break\n            parsed = json.loads(data)\n            tokens.append(parsed[\"token\"])\n\n    # Verify we got multiple tokens (streaming worked)\n    assert len(tokens) > 1, f\"Expected multiple tokens, got {len(tokens)}\"\n\n    # Verify tokens form a coherent response\n    full_response = \"\".join(tokens)\n    assert len(full_response) > 10, \"Response too short\"\n    assert \"Hello\" in full_response or \"hello\" in full_response.lower()\n\n    print(f\"Streaming chat: OK (received {len(tokens)} tokens)\")\n\n\ndef test_sync_chat():\n    \"\"\"Test the non-streaming chat endpoint.\"\"\"\n    base_url = os.environ.get(\"STREAMING_CHAT_URL\", \"http://127.0.0.1:8000\")\n\n    response = requests.post(\n        f\"{base_url}/chat/sync\",\n        json={\"prompt\": \"hello\"}\n    )\n    assert response.status_code == 200\n    data = response.json()\n    assert \"response\" in data\n    assert len(data[\"response\"]) > 10\n\n    print(\"Sync chat: OK\")\n\n\ndef test_thinking_mode():\n    \"\"\"Test streaming with thinking phase enabled.\"\"\"\n    base_url = os.environ.get(\"STREAMING_CHAT_URL\", \"http://127.0.0.1:8000\")\n\n    response = requests.post(\n        f\"{base_url}/chat\",\n        json={\"prompt\": \"hello\", \"thinking\": True},\n        stream=True\n    )\n    assert response.status_code == 200\n\n    tokens = []\n    for line in response.iter_lines(decode_unicode=True):\n        if line.startswith(\"data: \"):\n            data = line[6:]\n            if data == \"[DONE]\":\n                break\n            parsed = json.loads(data)\n            tokens.append(parsed[\"token\"])\n\n    full_response = \"\".join(tokens)\n    assert \"[thinking\" in full_response, \"Thinking phase not present\"\n    assert \"...]\" in full_response or \"..]\\n\" in full_response.replace(\".\", \"\"), \\\n        \"Thinking dots not present\"\n\n    print(\"Thinking mode: OK\")\n\n\ndef test_different_prompts():\n    \"\"\"Test that different prompts get different responses.\"\"\"\n    base_url = os.environ.get(\"STREAMING_CHAT_URL\", \"http://127.0.0.1:8000\")\n\n    prompts = [\"hello\", \"explain dirty workers\", \"how does streaming work?\"]\n    responses = []\n\n    for prompt in prompts:\n        response = requests.post(\n            f\"{base_url}/chat/sync\",\n            json={\"prompt\": prompt}\n        )\n        assert response.status_code == 200\n        responses.append(response.json()[\"response\"])\n\n    # Verify responses are different\n    assert len(set(responses)) == len(responses), \\\n        \"Expected different responses for different prompts\"\n\n    print(\"Different prompts: OK\")\n\n\ndef test_sse_format():\n    \"\"\"Test that SSE format is correct.\"\"\"\n    base_url = os.environ.get(\"STREAMING_CHAT_URL\", \"http://127.0.0.1:8000\")\n\n    response = requests.post(\n        f\"{base_url}/chat\",\n        json={\"prompt\": \"hello\"},\n        stream=True\n    )\n\n    raw_lines = []\n    for line in response.iter_lines(decode_unicode=True):\n        raw_lines.append(line)\n\n    # Check SSE format: lines should be \"data: ...\" or empty\n    for line in raw_lines:\n        assert line == \"\" or line.startswith(\"data: \"), \\\n            f\"Invalid SSE line: {line}\"\n\n    # Should end with [DONE]\n    data_lines = [line for line in raw_lines if line.startswith(\"data: \")]\n    assert data_lines[-1] == \"data: [DONE]\", \"Missing [DONE] terminator\"\n\n    print(\"SSE format: OK\")\n\n\nif __name__ == \"__main__\":\n    test_health_endpoint()\n    test_streaming_chat()\n    test_sync_chat()\n    test_thinking_mode()\n    test_different_prompts()\n    test_sse_format()\n    print(\"\\nAll tests passed!\")\n"
  },
  {
    "path": "examples/supervisor.conf",
    "content": "[program:gunicorn]\ncommand=/usr/local/bin/gunicorn main:application -c /path/to/project/gunicorn.conf.py\ndirectory=/path/to/project\nuser=nobody\nautorestart=true\nredirect_stderr=true\n"
  },
  {
    "path": "examples/test.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n#\n# Example code from Eventlet sources\n\nfrom wsgiref.validate import validator\n\nfrom gunicorn import __version__\n\n\n@validator\ndef app(environ, start_response):\n    \"\"\"Simplest possible application object\"\"\"\n\n    data = b'Hello, World!\\n'\n    status = '200 OK'\n\n    response_headers = [\n        ('Content-type', 'text/plain'),\n        ('Content-Length', str(len(data))),\n        ('X-Gunicorn-Version', __version__),\n        ('Foo', 'B\\u00e5r'),  # Foo: Bår\n    ]\n    start_response(status, response_headers)\n    return iter([data])\n"
  },
  {
    "path": "examples/timeout.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport sys\nimport time\n\n\ndef app(environ, start_response):\n    \"\"\"Application which pauses 35 seconds before responding. the worker\n    will timeout in default case.\"\"\"\n    data = b'Hello, World!\\n'\n    status = '200 OK'\n    response_headers = [\n        ('Content-type', 'text/plain'),\n        ('Content-Length', str(len(data))),\n    ]\n    sys.stdout.write('request will timeout')\n    sys.stdout.flush()\n    time.sleep(35)\n    start_response(status, response_headers)\n    return iter([data])\n"
  },
  {
    "path": "examples/websocket/gevent_websocket.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport collections\nimport errno\nimport re\nimport hashlib\nimport base64\nfrom base64 import b64encode, b64decode\nimport socket\nimport struct\nimport logging\nfrom socket import error as SocketError\n\nimport gevent\nfrom gunicorn.workers.base_async import ALREADY_HANDLED\n\nlogger = logging.getLogger(__name__)\n\nWS_KEY = b\"258EAFA5-E914-47DA-95CA-C5AB0DC85B11\"\n\nclass WebSocketWSGI:\n    def __init__(self, handler):\n        self.handler = handler\n\n    def verify_client(self, ws):\n        pass\n\n    def _get_key_value(self, key_value):\n        if not key_value:\n            return\n        key_number = int(re.sub(\"\\\\D\", \"\", key_value))\n        spaces = re.subn(\" \", \"\", key_value)[1]\n        if key_number % spaces != 0:\n            return\n        part = key_number / spaces\n        return part\n\n    def __call__(self, environ, start_response):\n        if not (environ.get('HTTP_CONNECTION').find('Upgrade') != -1 and\n            environ['HTTP_UPGRADE'].lower() == 'websocket'):\n            # need to check a few more things here for true compliance\n            start_response('400 Bad Request', [('Connection','close')])\n            return []\n\n        sock = environ['gunicorn.socket']\n\n        version = environ.get('HTTP_SEC_WEBSOCKET_VERSION')\n\n        ws = WebSocket(sock, environ, version)\n\n        handshake_reply = (\"HTTP/1.1 101 Switching Protocols\\r\\n\"\n                   \"Upgrade: websocket\\r\\n\"\n                   \"Connection: Upgrade\\r\\n\")\n\n        key = environ.get('HTTP_SEC_WEBSOCKET_KEY')\n        if key:\n            ws_key = base64.b64decode(key)\n            if len(ws_key) != 16:\n                start_response('400 Bad Request', [('Connection','close')])\n                return []\n\n            protocols = []\n            subprotocols = environ.get('HTTP_SEC_WEBSOCKET_PROTOCOL')\n            ws_protocols = []\n            if subprotocols:\n                for s in subprotocols.split(','):\n                    s = s.strip()\n                    if s in protocols:\n                        ws_protocols.append(s)\n            if ws_protocols:\n                handshake_reply += 'Sec-WebSocket-Protocol: %s\\r\\n' % ', '.join(ws_protocols)\n\n            exts = []\n            extensions = environ.get('HTTP_SEC_WEBSOCKET_EXTENSIONS')\n            ws_extensions = []\n            if extensions:\n                for ext in extensions.split(','):\n                    ext = ext.strip()\n                    if ext in exts:\n                        ws_extensions.append(ext)\n            if ws_extensions:\n                handshake_reply += 'Sec-WebSocket-Extensions: %s\\r\\n' % ', '.join(ws_extensions)\n\n            key_hash = hashlib.sha1()\n            key_hash.update(key.encode())\n            key_hash.update(WS_KEY)\n\n            handshake_reply +=  (\n                \"Sec-WebSocket-Origin: %s\\r\\n\"\n                \"Sec-WebSocket-Location: ws://%s%s\\r\\n\"\n                \"Sec-WebSocket-Version: %s\\r\\n\"\n                \"Sec-WebSocket-Accept: %s\\r\\n\\r\\n\"\n                 % (\n                    environ.get('HTTP_ORIGIN'),\n                    environ.get('HTTP_HOST'),\n                    ws.path,\n                    version,\n                    base64.b64encode(key_hash.digest()).decode()\n                ))\n\n        else:\n\n            handshake_reply += (\n                       \"WebSocket-Origin: %s\\r\\n\"\n                       \"WebSocket-Location: ws://%s%s\\r\\n\\r\\n\" % (\n                            environ.get('HTTP_ORIGIN'),\n                            environ.get('HTTP_HOST'),\n                            ws.path))\n\n        sock.sendall(handshake_reply.encode())\n\n        try:\n            self.handler(ws)\n        except BrokenPipeError:\n            pass\n        else:\n            raise\n        # use this undocumented feature of grainbows to ensure that it\n        # doesn't barf on the fact that we didn't call start_response\n        return ALREADY_HANDLED\n\nclass WebSocket:\n    \"\"\"A websocket object that handles the details of\n    serialization/deserialization to the socket.\n\n    The primary way to interact with a :class:`WebSocket` object is to\n    call :meth:`send` and :meth:`wait` in order to pass messages back\n    and forth with the browser.  Also available are the following\n    properties:\n\n    path\n        The path value of the request.  This is the same as the WSGI PATH_INFO variable, but more convenient.\n    protocol\n        The value of the Websocket-Protocol header.\n    origin\n        The value of the 'Origin' header.\n    environ\n        The full WSGI environment for this request.\n\n    \"\"\"\n    def __init__(self, sock, environ, version=76):\n        \"\"\"\n        :param socket: The gevent socket\n        :type socket: :class:`gevent.socket.socket`\n        :param environ: The wsgi environment\n        :param version: The WebSocket spec version to follow (default is 76)\n        \"\"\"\n        self.socket = sock\n        self.origin = environ.get('HTTP_ORIGIN')\n        self.protocol = environ.get('HTTP_WEBSOCKET_PROTOCOL')\n        self.path = environ.get('PATH_INFO')\n        self.environ = environ\n        self.version = version\n        self.websocket_closed = False\n        self._buf = \"\"\n        self._msgs = collections.deque()\n        #self._sendlock = semaphore.Semaphore()\n\n    @staticmethod\n    def encode_hybi(buf, opcode, base64=False):\n        \"\"\" Encode a HyBi style WebSocket frame.\n        Optional opcode:\n            0x0 - continuation\n            0x1 - text frame (base64 encode buf)\n            0x2 - binary frame (use raw buf)\n            0x8 - connection close\n            0x9 - ping\n            0xA - pong\n        \"\"\"\n        if base64:\n            buf = b64encode(buf)\n        else:\n            buf = buf.encode()\n\n        b1 = 0x80 | (opcode & 0x0f) # FIN + opcode\n        payload_len = len(buf)\n        if payload_len <= 125:\n            header = struct.pack('>BB', b1, payload_len)\n        elif payload_len > 125 and payload_len < 65536:\n            header = struct.pack('>BBH', b1, 126, payload_len)\n        elif payload_len >= 65536:\n            header = struct.pack('>BBQ', b1, 127, payload_len)\n\n        #print(\"Encoded: %s\" % repr(header + buf))\n\n        return header + buf, len(header), 0\n\n    @staticmethod\n    def decode_hybi(buf, base64=False):\n        \"\"\" Decode HyBi style WebSocket packets.\n        Returns:\n            {'fin'          : 0_or_1,\n             'opcode'       : number,\n             'mask'         : 32_bit_number,\n             'hlen'         : header_bytes_number,\n             'length'       : payload_bytes_number,\n             'payload'      : decoded_buffer,\n             'left'         : bytes_left_number,\n             'close_code'   : number,\n             'close_reason' : string}\n        \"\"\"\n\n        f = {'fin'          : 0,\n             'opcode'       : 0,\n             'mask'         : 0,\n             'hlen'         : 2,\n             'length'       : 0,\n             'payload'      : None,\n             'left'         : 0,\n             'close_code'   : None,\n             'close_reason' : None}\n\n        blen = len(buf)\n        f['left'] = blen\n\n        if blen < f['hlen']:\n            return f # Incomplete frame header\n\n        b1, b2 = struct.unpack_from(\">BB\", buf)\n        f['opcode'] = b1 & 0x0f\n        f['fin'] = (b1 & 0x80) >> 7\n        has_mask = (b2 & 0x80) >> 7\n\n        f['length'] = b2 & 0x7f\n\n        if f['length'] == 126:\n            f['hlen'] = 4\n            if blen < f['hlen']:\n                return f # Incomplete frame header\n            (f['length'],) = struct.unpack_from('>xxH', buf)\n        elif f['length'] == 127:\n            f['hlen'] = 10\n            if blen < f['hlen']:\n                return f # Incomplete frame header\n            (f['length'],) = struct.unpack_from('>xxQ', buf)\n\n        full_len = f['hlen'] + has_mask * 4 + f['length']\n\n        if blen < full_len: # Incomplete frame\n            return f # Incomplete frame header\n\n        # Number of bytes that are part of the next frame(s)\n        f['left'] = blen - full_len\n\n        # Process 1 frame\n        if has_mask:\n            # unmask payload\n            f['mask'] = buf[f['hlen']:f['hlen']+4]\n            b = c = ''\n            if f['length'] >= 4:\n                data = struct.unpack('<I', buf[f['hlen']:f['hlen']+4])[0]\n                of1 = f['hlen']+4\n                b = ''\n                for i in range(0, int(f['length']/4)):\n                    mask = struct.unpack('<I', buf[of1+4*i:of1+4*(i+1)])[0]\n                    b += struct.pack('I', data ^ mask)\n\n            if f['length'] % 4:\n                l = f['length'] % 4\n                of1 = f['hlen']\n                of2 = full_len - l\n                c = ''\n                for i in range(0, l):\n                    mask = struct.unpack('B', buf[of1 + i])[0]\n                    data = struct.unpack('B', buf[of2 + i])[0]\n                    c += chr(data ^ mask)\n\n            f['payload'] = b + c\n        else:\n            print(\"Unmasked frame: %s\" % repr(buf))\n            f['payload'] = buf[(f['hlen'] + has_mask * 4):full_len]\n\n        if base64 and f['opcode'] in [1, 2]:\n            try:\n                f['payload'] = b64decode(f['payload'])\n            except:\n                print(\"Exception while b64decoding buffer: %s\" %\n                        repr(buf))\n                raise\n\n        if f['opcode'] == 0x08:\n            if f['length'] >= 2:\n                f['close_code'] = struct.unpack_from(\">H\", f['payload'])\n            if f['length'] > 3:\n                f['close_reason'] = f['payload'][2:]\n\n        return f\n\n\n    @staticmethod\n    def _pack_message(message):\n        \"\"\"Pack the message inside ``00`` and ``FF``\n\n        As per the dataframing section (5.3) for the websocket spec\n        \"\"\"\n        if isinstance(message, str):\n            message = message.encode('utf-8')\n        packed = \"\\x00%s\\xFF\" % message\n        return packed\n\n    def _parse_messages(self):\n        \"\"\" Parses for messages in the buffer *buf*.  It is assumed that\n        the buffer contains the start character for a message, but that it\n        may contain only part of the rest of the message.\n\n        Returns an array of messages, and the buffer remainder that\n        didn't contain any full messages.\"\"\"\n        msgs = []\n        end_idx = 0\n        buf = self._buf\n        while buf:\n            if self.version in ['7', '8', '13']:\n                frame = self.decode_hybi(buf, base64=False)\n                #print(\"Received buf: %s, frame: %s\" % (repr(buf), frame))\n\n                if frame['payload'] == None:\n                    break\n                else:\n                    if frame['opcode'] == 0x8: # connection close\n                        self.websocket_closed = True\n                        break\n                    #elif frame['opcode'] == 0x1:\n                    else:\n                        msgs.append(frame['payload']);\n                        #msgs.append(frame['payload'].decode('utf-8', 'replace'));\n                        #buf = buf[-frame['left']:]\n                        if frame['left']:\n                            buf = buf[-frame['left']:]\n                        else:\n                            buf = ''\n\n\n            else:\n                frame_type = ord(buf[0])\n                if frame_type == 0:\n                    # Normal message.\n                    end_idx = buf.find(\"\\xFF\")\n                    if end_idx == -1: #pragma NO COVER\n                        break\n                    msgs.append(buf[1:end_idx].decode('utf-8', 'replace'))\n                    buf = buf[end_idx+1:]\n                elif frame_type == 255:\n                    # Closing handshake.\n                    assert ord(buf[1]) == 0, \"Unexpected closing handshake: %r\" % buf\n                    self.websocket_closed = True\n                    break\n                else:\n                    raise ValueError(\"Don't understand how to parse this type of message: %r\" % buf)\n        self._buf = buf\n        return msgs\n\n    def send(self, message):\n        \"\"\"Send a message to the browser.\n\n        *message* should be convertible to a string; unicode objects should be\n        encodable as utf-8.  Raises socket.error with errno of 32\n        (broken pipe) if the socket has already been closed by the client.\"\"\"\n        if self.version in ['7', '8', '13']:\n            packed, lenhead, lentail = self.encode_hybi(message, opcode=0x01, base64=False)\n        else:\n            packed = self._pack_message(message)\n        # if two greenthreads are trying to send at the same time\n        # on the same socket, sendlock prevents interleaving and corruption\n        #self._sendlock.acquire()\n        try:\n            self.socket.sendall(packed)\n        finally:\n            pass\n            #self._sendlock.release()\n\n    def wait(self):\n        \"\"\"Waits for and deserializes messages.\n\n        Returns a single message; the oldest not yet processed. If the client\n        has already closed the connection, returns None.  This is different\n        from normal socket behavior because the empty string is a valid\n        websocket message.\"\"\"\n        while not self._msgs:\n            # Websocket might be closed already.\n            if self.websocket_closed:\n                return None\n            # no parsed messages, must mean buf needs more data\n            delta = self.socket.recv(8096)\n            if delta == b'':\n                return None\n            self._buf += delta\n            msgs = self._parse_messages()\n            self._msgs.extend(msgs)\n        return self._msgs.popleft()\n\n    def _send_closing_frame(self, ignore_send_errors=False):\n        \"\"\"Sends the closing frame to the client, if required.\"\"\"\n        if self.version in ['7', '8', '13'] and not self.websocket_closed:\n            msg = ''\n            #if code != None:\n            #    msg = struct.pack(\">H%ds\" % (len(reason)), code)\n\n            buf, h, t = self.encode_hybi(msg, opcode=0x08, base64=False)\n            self.socket.sendall(buf)\n            self.websocket_closed = True\n\n        elif self.version == 76 and not self.websocket_closed:\n            try:\n                self.socket.sendall(b\"\\xff\\x00\")\n            except SocketError:\n                # Sometimes, like when the remote side cuts off the connection,\n                # we don't care about this.\n                if not ignore_send_errors: #pragma NO COVER\n                    raise\n            self.websocket_closed = True\n\n    def close(self):\n        \"\"\"Forcibly close the websocket; generally it is preferable to\n        return from the handler method.\"\"\"\n        self._send_closing_frame()\n        self.socket.shutdown(True)\n        self.socket.close()\n\n\n# demo app\nimport os\nimport random\ndef handle(ws):\n    \"\"\"  This is the websocket handler function.  Note that we\n    can dispatch based on path in here, too.\"\"\"\n    if ws.path == '/echo':\n        while True:\n            m = ws.wait()\n            if m is None:\n                break\n            ws.send(m)\n\n    elif ws.path == '/data':\n        for i in range(10000):\n            ws.send(\"0 %s %s\\n\" % (i, random.random()))\n            gevent.sleep(0.1)\n\nwsapp = WebSocketWSGI(handle)\ndef app(environ, start_response):\n    \"\"\" This resolves to the web page or the websocket depending on\n    the path.\"\"\"\n    if environ['PATH_INFO'] == '/' or environ['PATH_INFO'] == \"\":\n        data = open(os.path.join(\n                     os.path.dirname(__file__),\n                     'websocket.html')).read()\n        data = data % environ\n        start_response('200 OK', [('Content-Type', 'text/html'),\n                                 ('Content-Length', str(len(data)))])\n        return [data.encode()]\n    else:\n        return wsapp(environ, start_response)\n"
  },
  {
    "path": "examples/websocket/websocket.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n<!-- idea and code swiped from\nhttp://assorted.svn.sourceforge.net/viewvc/assorted/real-time-plotter/trunk/src/rtp.html?view=markup -->\n<script src=\"http://ajax.googleapis.com/ajax/libs/jquery/1.4.1/jquery.min.js\"></script>\n<script src=\"http://people.iola.dk/olau/flot/jquery.flot.js\"></script>\n<script>\nwindow.onload = function() {\n    var data = {};\n    var s = new WebSocket(\"ws://%(HTTP_HOST)s/data\");\n    s.onopen = function() {\n        //alert('open');\n        s.send('hi');\n    };\n    s.onmessage = function(e) {\n      //alert('got ' + e.data);\n      var lines = e.data.split('\\n');\n      for (var i = 0; i < lines.length - 1; i++) {\n        var parts = lines[i].split(' ');\n        var d = parts[0], x = parseFloat(parts[1]), y = parseFloat(parts[2]);\n        if (!(d in data)) data[d] = [];\n        data[d].push([x,y]);\n      }\n      var plots = [];\n      for (var d in data) plots.push( { data: data[d].slice(data[d].length - 200) } );\n      $.plot( $(\"#holder\"), plots,\n              {\n                series: {\n                  lines: { show: true, fill: true },\n                },\n                yaxis: { min: 0 },\n              } );\n\n      s.send('');\n    };\n};\n</script>\n</head>\n<body>\n<h3>Plot</h3>\n<div id=\"holder\" style=\"width:600px;height:300px\"></div>\n</body>\n</html>\n"
  },
  {
    "path": "examples/when_ready.conf.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport signal\nimport commands\nimport threading\nimport time\n\nmax_mem = 100000\n\nclass MemoryWatch(threading.Thread):\n\n    def __init__(self, server, max_mem):\n        super().__init__()\n        self.daemon = True\n        self.server = server\n        self.max_mem = max_mem\n        self.timeout = server.timeout / 2\n\n    def memory_usage(self, pid):\n        try:\n            out = commands.getoutput(\"ps -o rss -p %s\" % pid)\n        except OSError:\n            return -1\n        used_mem = sum(int(x) for x in out.split('\\n')[1:])\n        return used_mem\n\n    def run(self):\n        while True:\n            for (pid, worker) in list(self.server.WORKERS.items()):\n                if self.memory_usage(pid) > self.max_mem:\n                    self.server.log.info(\"Pid %s killed (memory usage > %s)\",\n                        pid, self.max_mem)\n                    self.server.kill_worker(pid, signal.SIGTERM)\n            time.sleep(self.timeout)\n\n\ndef when_ready(server):\n    mw = MemoryWatch(server, max_mem)\n    mw.start()\n"
  },
  {
    "path": "gunicorn/__init__.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nversion_info = (25, 1, 0)\n__version__ = \".\".join([str(v) for v in version_info])\nSERVER = \"gunicorn\"\nSERVER_SOFTWARE = \"%s/%s\" % (SERVER, __version__)\n"
  },
  {
    "path": "gunicorn/__main__.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.app.wsgiapp import run\n\nif __name__ == \"__main__\":\n    # see config.py - argparse defaults to basename(argv[0]) == \"__main__.py\"\n    # todo: let runpy.run_module take care of argv[0] rewriting\n    run(prog=\"gunicorn\")\n"
  },
  {
    "path": "gunicorn/app/__init__.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n"
  },
  {
    "path": "gunicorn/app/base.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\nimport importlib.util\nimport importlib.machinery\nimport os\nimport sys\nimport traceback\n\nfrom gunicorn import util\nfrom gunicorn.arbiter import Arbiter\nfrom gunicorn.config import Config, get_default_config_file\nfrom gunicorn import debug\n\n\nclass BaseApplication:\n    \"\"\"\n    An application interface for configuring and loading\n    the various necessities for any given web framework.\n    \"\"\"\n    def __init__(self, usage=None, prog=None):\n        self.usage = usage\n        self.cfg = None\n        self.callable = None\n        self.prog = prog\n        self.logger = None\n        self.do_load_config()\n\n    def do_load_config(self):\n        \"\"\"\n        Loads the configuration\n        \"\"\"\n        try:\n            self.load_default_config()\n            self.load_config()\n        except Exception as e:\n            print(\"\\nError: %s\" % str(e), file=sys.stderr)\n            sys.stderr.flush()\n            sys.exit(1)\n\n    def load_default_config(self):\n        # init configuration\n        self.cfg = Config(self.usage, prog=self.prog)\n\n    def init(self, parser, opts, args):\n        raise NotImplementedError\n\n    def load(self):\n        raise NotImplementedError\n\n    def load_config(self):\n        \"\"\"\n        This method is used to load the configuration from one or several input(s).\n        Custom Command line, configuration file.\n        You have to override this method in your class.\n        \"\"\"\n        raise NotImplementedError\n\n    def reload(self):\n        self.do_load_config()\n        if self.cfg.spew:\n            debug.spew()\n\n    def wsgi(self):\n        if self.callable is None:\n            self.callable = self.load()\n        return self.callable\n\n    def run(self):\n        try:\n            Arbiter(self).run()\n        except RuntimeError as e:\n            print(\"\\nError: %s\\n\" % e, file=sys.stderr)\n            sys.stderr.flush()\n            sys.exit(1)\n\n\nclass Application(BaseApplication):\n\n    # 'init' and 'load' methods are implemented by WSGIApplication.\n    # pylint: disable=abstract-method\n\n    def chdir(self):\n        # chdir to the configured path before loading,\n        # default is the current dir\n        os.chdir(self.cfg.chdir)\n\n        # add the path to sys.path\n        if self.cfg.chdir not in sys.path:\n            sys.path.insert(0, self.cfg.chdir)\n\n    def get_config_from_filename(self, filename):\n\n        if not os.path.exists(filename):\n            raise RuntimeError(\"%r doesn't exist\" % filename)\n\n        ext = os.path.splitext(filename)[1]\n\n        try:\n            module_name = '__config__'\n            if ext in [\".py\", \".pyc\"]:\n                spec = importlib.util.spec_from_file_location(module_name, filename)\n            else:\n                msg = \"configuration file should have a valid Python extension.\\n\"\n                util.warn(msg)\n                loader_ = importlib.machinery.SourceFileLoader(module_name, filename)\n                spec = importlib.util.spec_from_file_location(module_name, filename, loader=loader_)\n            mod = importlib.util.module_from_spec(spec)\n            sys.modules[module_name] = mod\n            spec.loader.exec_module(mod)\n        except Exception:\n            print(\"Failed to read config file: %s\" % filename, file=sys.stderr)\n            traceback.print_exc()\n            sys.stderr.flush()\n            sys.exit(1)\n\n        return vars(mod)\n\n    def get_config_from_module_name(self, module_name):\n        return vars(importlib.import_module(module_name))\n\n    def load_config_from_module_name_or_filename(self, location):\n        \"\"\"\n        Loads the configuration file: the file is a python file, otherwise raise an RuntimeError\n        Exception or stop the process if the configuration file contains a syntax error.\n        \"\"\"\n\n        if location.startswith(\"python:\"):\n            module_name = location[len(\"python:\"):]\n            cfg = self.get_config_from_module_name(module_name)\n        else:\n            if location.startswith(\"file:\"):\n                filename = location[len(\"file:\"):]\n            else:\n                filename = location\n            cfg = self.get_config_from_filename(filename)\n\n        for k, v in cfg.items():\n            # Ignore unknown names\n            if k not in self.cfg.settings:\n                continue\n            try:\n                self.cfg.set(k.lower(), v)\n            except Exception:\n                print(\"Invalid value for %s: %s\\n\" % (k, v), file=sys.stderr)\n                sys.stderr.flush()\n                raise\n\n        return cfg\n\n    def load_config_from_file(self, filename):\n        return self.load_config_from_module_name_or_filename(location=filename)\n\n    def load_config(self):\n        # parse console args\n        parser = self.cfg.parser()\n        args = parser.parse_args()\n\n        # optional settings from apps\n        cfg = self.init(parser, args, args.args)\n\n        # set up import paths and follow symlinks\n        self.chdir()\n\n        # Load up the any app specific configuration\n        if cfg:\n            for k, v in cfg.items():\n                self.cfg.set(k.lower(), v)\n\n        env_args = parser.parse_args(self.cfg.get_cmd_args_from_env())\n\n        if args.config:\n            self.load_config_from_file(args.config)\n        elif env_args.config:\n            self.load_config_from_file(env_args.config)\n        else:\n            default_config = get_default_config_file()\n            if default_config is not None:\n                self.load_config_from_file(default_config)\n\n        # Load up environment configuration\n        for k, v in vars(env_args).items():\n            if v is None:\n                continue\n            if k == \"args\":\n                continue\n            self.cfg.set(k.lower(), v)\n\n        # Lastly, update the configuration with any command line settings.\n        for k, v in vars(args).items():\n            if v is None:\n                continue\n            if k == \"args\":\n                continue\n            self.cfg.set(k.lower(), v)\n\n        # current directory might be changed by the config now\n        # set up import paths and follow symlinks\n        self.chdir()\n\n    def run(self):\n        if self.cfg.print_config:\n            print(self.cfg)\n\n        if self.cfg.print_config or self.cfg.check_config:\n            try:\n                self.load()\n            except Exception:\n                msg = \"\\nError while loading the application:\\n\"\n                print(msg, file=sys.stderr)\n                traceback.print_exc()\n                sys.stderr.flush()\n                sys.exit(1)\n            sys.exit(0)\n\n        if self.cfg.spew:\n            debug.spew()\n\n        if self.cfg.daemon:\n            if os.environ.get('NOTIFY_SOCKET'):\n                msg = \"Warning: you shouldn't specify `daemon = True`\" \\\n                      \" when launching by systemd with `Type = notify`\"\n                print(msg, file=sys.stderr, flush=True)\n\n            util.daemonize(self.cfg.enable_stdio_inheritance)\n\n        # set python paths\n        if self.cfg.pythonpath:\n            paths = self.cfg.pythonpath.split(\",\")\n            for path in paths:\n                pythonpath = os.path.abspath(path)\n                if pythonpath not in sys.path:\n                    sys.path.insert(0, pythonpath)\n\n        super().run()\n"
  },
  {
    "path": "gunicorn/app/pasterapp.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport configparser\nimport os\n\nfrom paste.deploy import loadapp\n\nfrom gunicorn.app.wsgiapp import WSGIApplication\nfrom gunicorn.config import get_default_config_file\n\n\ndef get_wsgi_app(config_uri, name=None, defaults=None):\n    if ':' not in config_uri:\n        config_uri = \"config:%s\" % config_uri\n\n    return loadapp(\n        config_uri,\n        name=name,\n        relative_to=os.getcwd(),\n        global_conf=defaults,\n    )\n\n\ndef has_logging_config(config_file):\n    parser = configparser.ConfigParser()\n    parser.read([config_file])\n    return parser.has_section('loggers')\n\n\ndef serve(app, global_conf, **local_conf):\n    \"\"\"\\\n    A Paste Deployment server runner.\n\n    Example configuration:\n\n        [server:main]\n        use = egg:gunicorn#main\n        host = 127.0.0.1\n        port = 5000\n    \"\"\"\n    config_file = global_conf['__file__']\n    gunicorn_config_file = local_conf.pop('config', None)\n\n    host = local_conf.pop('host', '')\n    port = local_conf.pop('port', '')\n    if host and port:\n        local_conf['bind'] = '%s:%s' % (host, port)\n    elif host:\n        local_conf['bind'] = host.split(',')\n\n    class PasterServerApplication(WSGIApplication):\n        def load_config(self):\n            self.cfg.set(\"default_proc_name\", config_file)\n\n            if has_logging_config(config_file):\n                self.cfg.set(\"logconfig\", config_file)\n\n            if gunicorn_config_file:\n                self.load_config_from_file(gunicorn_config_file)\n            else:\n                default_gunicorn_config_file = get_default_config_file()\n                if default_gunicorn_config_file is not None:\n                    self.load_config_from_file(default_gunicorn_config_file)\n\n            for k, v in local_conf.items():\n                if v is not None:\n                    self.cfg.set(k.lower(), v)\n\n        def load(self):\n            return app\n\n    PasterServerApplication().run()\n"
  },
  {
    "path": "gunicorn/app/wsgiapp.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport os\n\nfrom gunicorn.errors import ConfigError\nfrom gunicorn.app.base import Application\nfrom gunicorn import util\n\n\nclass WSGIApplication(Application):\n    def init(self, parser, opts, args):\n        self.app_uri = None\n\n        if opts.paste:\n            from .pasterapp import has_logging_config\n\n            config_uri = os.path.abspath(opts.paste)\n            config_file = config_uri.split('#')[0]\n\n            if not os.path.exists(config_file):\n                raise ConfigError(\"%r not found\" % config_file)\n\n            self.cfg.set(\"default_proc_name\", config_file)\n            self.app_uri = config_uri\n\n            if has_logging_config(config_file):\n                self.cfg.set(\"logconfig\", config_file)\n\n            return\n\n        if len(args) > 0:\n            self.cfg.set(\"default_proc_name\", args[0])\n            self.app_uri = args[0]\n\n    def load_config(self):\n        super().load_config()\n\n        if self.app_uri is None:\n            if self.cfg.wsgi_app is not None:\n                self.app_uri = self.cfg.wsgi_app\n            else:\n                raise ConfigError(\"No application module specified.\")\n\n    def load_wsgiapp(self):\n        return util.import_app(self.app_uri)\n\n    def load_pasteapp(self):\n        from .pasterapp import get_wsgi_app\n        return get_wsgi_app(self.app_uri, defaults=self.cfg.paste_global_conf)\n\n    def load(self):\n        if self.cfg.paste is not None:\n            return self.load_pasteapp()\n        else:\n            return self.load_wsgiapp()\n\n\ndef run(prog=None):\n    \"\"\"\\\n    The ``gunicorn`` command line runner for launching Gunicorn with\n    generic WSGI applications.\n    \"\"\"\n    from gunicorn.app.wsgiapp import WSGIApplication\n    WSGIApplication(\"%(prog)s [OPTIONS] [APP_MODULE]\", prog=prog).run()\n\n\nif __name__ == '__main__':\n    run()\n"
  },
  {
    "path": "gunicorn/arbiter.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\nimport errno\nimport os\nimport queue\nimport random\nimport signal\nimport sys\nimport time\nimport traceback\nimport socket\n\nfrom gunicorn.errors import HaltServer, AppImportError\nfrom gunicorn.pidfile import Pidfile\nfrom gunicorn import sock, systemd, util\n\nfrom gunicorn import __version__, SERVER_SOFTWARE\n\n# gunicorn.dirty is imported lazily in spawn_dirty_arbiter() for gevent compatibility\n\n\nclass Arbiter:\n    \"\"\"\n    Arbiter maintain the workers processes alive. It launches or\n    kills them if needed. It also manages application reloading\n    via SIGHUP/USR2.\n    \"\"\"\n\n    # A flag indicating if a worker failed to\n    # to boot. If a worker process exist with\n    # this error code, the arbiter will terminate.\n    WORKER_BOOT_ERROR = 3\n\n    # A flag indicating if an application failed to be loaded\n    APP_LOAD_ERROR = 4\n\n    START_CTX = {}\n\n    LISTENERS = []\n    WORKERS = {}\n\n    # Sentinel value for non-signal wakeups\n    WAKEUP_REQUEST = signal.NSIG\n\n    SIGNALS = [getattr(signal, \"SIG%s\" % x)\n               for x in \"HUP QUIT INT TERM TTIN TTOU USR1 USR2 WINCH\".split()]\n    SIG_NAMES = dict(\n        (getattr(signal, name), name[3:].lower()) for name in dir(signal)\n        if name[:3] == \"SIG\" and name[3] != \"_\"\n    )\n\n    def __init__(self, app):\n        os.environ[\"SERVER_SOFTWARE\"] = SERVER_SOFTWARE\n\n        self._num_workers = None\n        self._last_logged_active_worker_count = None\n        self.log = None\n\n        # Signal queue - SimpleQueue is reentrant-safe for signal handlers\n        self.SIG_QUEUE = queue.SimpleQueue()\n\n        self.setup(app)\n\n        self.pidfile = None\n        self.systemd = False\n        self.worker_age = 0\n        self.reexec_pid = 0\n        self.master_pid = 0\n        self.master_name = \"Master\"\n\n        # Dirty arbiter process\n        self.dirty_arbiter_pid = 0\n        self.dirty_arbiter = None\n        self.dirty_pidfile = None  # Well-known location for orphan detection\n\n        # Control socket server\n        self._control_server = None\n\n        # Stats tracking\n        self._stats = {\n            'start_time': None,\n            'workers_spawned': 0,\n            'workers_killed': 0,\n            'reloads': 0,\n        }\n\n        cwd = util.getcwd()\n\n        args = sys.argv[:]\n        args.insert(0, sys.executable)\n\n        # init start context\n        self.START_CTX = {\n            \"args\": args,\n            \"cwd\": cwd,\n            0: sys.executable\n        }\n\n    def _get_num_workers(self):\n        return self._num_workers\n\n    def _set_num_workers(self, value):\n        old_value = self._num_workers\n        self._num_workers = value\n        self.cfg.nworkers_changed(self, value, old_value)\n    num_workers = property(_get_num_workers, _set_num_workers)\n\n    def setup(self, app):\n        self.app = app\n        self.cfg = app.cfg\n\n        if self.log is None:\n            self.log = self.cfg.logger_class(app.cfg)\n\n        # reopen files\n        if 'GUNICORN_PID' in os.environ:\n            self.log.reopen_files()\n\n        self.worker_class = self.cfg.worker_class\n        self.address = self.cfg.address\n        self.num_workers = self.cfg.workers\n        self.timeout = self.cfg.timeout\n        self.proc_name = self.cfg.proc_name\n\n        self.log.debug('Current configuration:\\n{0}'.format(\n            '\\n'.join(\n                '  {0}: {1}'.format(config, value.value)\n                for config, value\n                in sorted(self.cfg.settings.items(),\n                          key=lambda setting: setting[1]))))\n\n        # set environment' variables\n        if self.cfg.env:\n            for k, v in self.cfg.env.items():\n                os.environ[k] = v\n\n        if self.cfg.preload_app:\n            self.app.wsgi()\n\n    def start(self):\n        \"\"\"\\\n        Initialize the arbiter. Start listening and set pidfile if needed.\n        \"\"\"\n        self.log.info(\"Starting gunicorn %s\", __version__)\n\n        # Initialize stats tracking\n        self._stats['start_time'] = time.time()\n\n        if 'GUNICORN_PID' in os.environ:\n            self.master_pid = int(os.environ.get('GUNICORN_PID'))\n            self.proc_name = self.proc_name + \".2\"\n            self.master_name = \"Master.2\"\n\n        self.pid = os.getpid()\n        if self.cfg.pidfile is not None:\n            pidname = self.cfg.pidfile\n            if self.master_pid != 0:\n                pidname += \".2\"\n            self.pidfile = Pidfile(pidname)\n            self.pidfile.create(self.pid)\n        self.cfg.on_starting(self)\n\n        self.init_signals()\n\n        if not self.LISTENERS:\n            fds = None\n            listen_fds = systemd.listen_fds()\n            if listen_fds:\n                self.systemd = True\n                fds = range(systemd.SD_LISTEN_FDS_START,\n                            systemd.SD_LISTEN_FDS_START + listen_fds)\n\n            elif self.master_pid:\n                fds = []\n                for fd in os.environ.pop('GUNICORN_FD').split(','):\n                    fds.append(int(fd))\n\n            if not (self.cfg.reuse_port and hasattr(socket, 'SO_REUSEPORT')):\n                self.LISTENERS = sock.create_sockets(self.cfg, self.log, fds)\n\n        listeners_str = \",\".join([str(lnr) for lnr in self.LISTENERS])\n        self.log.debug(\"Arbiter booted\")\n        self.log.info(\"Listening at: %s (%s)\", listeners_str, self.pid)\n        self.log.info(\"Using worker: %s\", self.cfg.worker_class_str)\n        systemd.sd_notify(\"READY=1\\nSTATUS=Gunicorn arbiter booted\", self.log)\n\n        # check worker class requirements\n        if hasattr(self.worker_class, \"check_config\"):\n            self.worker_class.check_config(self.cfg, self.log)\n\n        # Start dirty arbiter if configured\n        if self.cfg.dirty_workers > 0 and self.cfg.dirty_apps:\n            self.spawn_dirty_arbiter()\n\n        # Note: control socket server is started after initial workers spawn\n        # to avoid fork deadlocks with asyncio\n\n        self.cfg.when_ready(self)\n\n    def init_signals(self):\n        \"\"\"\\\n        Initialize master signal handling. Most of the signals\n        are queued. Child signals only wake up the master.\n        \"\"\"\n        self.log.close_on_exec()\n\n        # initialize all signals\n        for s in self.SIGNALS:\n            signal.signal(s, self.signal)\n        signal.signal(signal.SIGCHLD, self.signal_chld)\n\n    def signal(self, sig, frame):\n        \"\"\"Signal handler - NO LOGGING, just queue the signal.\"\"\"\n        self.SIG_QUEUE.put_nowait(sig)\n\n    def run(self):\n        \"Main master loop.\"\n        self.start()\n        util._setproctitle(\"master [%s]\" % self.proc_name)\n\n        try:\n            self.manage_workers()\n\n            # Start control socket server after initial workers are spawned\n            # to avoid fork deadlocks with asyncio\n            self._start_control_server()\n\n            while True:\n                self.maybe_promote_master()\n\n                # Wait for and process signals\n                for sig in self.wait_for_signals(timeout=1.0):\n                    if sig not in self.SIG_NAMES:\n                        self.log.info(\"Ignoring unknown signal: %s\", sig)\n                        continue\n\n                    signame = self.SIG_NAMES.get(sig)\n                    handler = getattr(self, \"handle_%s\" % signame, None)\n                    if not handler:\n                        self.log.error(\"Unhandled signal: %s\", signame)\n                        continue\n                    # Log SIGCHLD at debug level since it's frequent\n                    log_level = self.log.debug if sig == signal.SIGCHLD else self.log.info\n                    log_level(\"Handling signal: %s\", signame)\n                    handler()\n\n                self.murder_workers()\n                self.manage_workers()\n                self.manage_dirty_arbiter()\n        except (StopIteration, KeyboardInterrupt):\n            self.halt()\n        except HaltServer as inst:\n            self.halt(reason=inst.reason, exit_status=inst.exit_status)\n        except SystemExit:\n            raise\n        except Exception:\n            self.log.error(\"Unhandled exception in main loop\",\n                           exc_info=True)\n            self.stop(False)\n            if self.pidfile is not None:\n                self.pidfile.unlink()\n            sys.exit(-1)\n\n    def signal_chld(self, sig, frame):\n        \"\"\"SIGCHLD signal handler - NO LOGGING, just queue the signal.\"\"\"\n        self.SIG_QUEUE.put_nowait(sig)\n\n    def handle_chld(self):\n        \"\"\"SIGCHLD handling - called from main loop, safe to log.\"\"\"\n        self.reap_workers()\n        self.reap_dirty_arbiter()\n\n    # SIGCLD is an alias for SIGCHLD on Linux. The SIG_NAMES dict may map\n    # to either \"chld\" or \"cld\" depending on iteration order of dir(signal).\n    handle_cld = handle_chld\n\n    def handle_hup(self):\n        \"\"\"\\\n        HUP handling.\n        - Reload configuration\n        - Start the new worker processes with a new configuration\n        - Gracefully shutdown the old worker processes\n        \"\"\"\n        self.log.info(\"Hang up: %s\", self.master_name)\n        self.reload()\n        # Forward to dirty arbiter\n        if self.dirty_arbiter_pid:\n            self.kill_dirty_arbiter(signal.SIGHUP)\n\n    def handle_term(self):\n        \"SIGTERM handling\"\n        raise StopIteration\n\n    def handle_int(self):\n        \"SIGINT handling\"\n        self.stop(False)\n        raise StopIteration\n\n    def handle_quit(self):\n        \"SIGQUIT handling\"\n        self.stop(False)\n        raise StopIteration\n\n    def handle_ttin(self):\n        \"\"\"\\\n        SIGTTIN handling.\n        Increases the number of workers by one.\n        \"\"\"\n        self.num_workers += 1\n        self.manage_workers()\n\n    def handle_ttou(self):\n        \"\"\"\\\n        SIGTTOU handling.\n        Decreases the number of workers by one.\n        \"\"\"\n        if self.num_workers <= 1:\n            return\n        self.num_workers -= 1\n        self.manage_workers()\n\n    def handle_usr1(self):\n        \"\"\"\\\n        SIGUSR1 handling.\n        Kill all workers by sending them a SIGUSR1\n        \"\"\"\n        self.log.reopen_files()\n        self.kill_workers(signal.SIGUSR1)\n        # Forward to dirty arbiter\n        if self.dirty_arbiter_pid:\n            self.kill_dirty_arbiter(signal.SIGUSR1)\n\n    def handle_usr2(self):\n        \"\"\"\\\n        SIGUSR2 handling.\n        Creates a new arbiter/worker set as a fork of the current\n        arbiter without affecting old workers. Use this to do live\n        deployment with the ability to backout a change.\n        \"\"\"\n        self.reexec()\n\n    def handle_winch(self):\n        \"\"\"SIGWINCH handling\"\"\"\n        if self.cfg.daemon:\n            self.log.info(\"graceful stop of workers\")\n            self.num_workers = 0\n            self.kill_workers(signal.SIGTERM)\n        else:\n            self.log.debug(\"SIGWINCH ignored. Not daemonized\")\n\n    def maybe_promote_master(self):\n        if self.master_pid == 0:\n            return\n\n        if self.master_pid != os.getppid():\n            self.log.info(\"Master has been promoted.\")\n            # reset master infos\n            self.master_name = \"Master\"\n            self.master_pid = 0\n            self.proc_name = self.cfg.proc_name\n            del os.environ['GUNICORN_PID']\n            # rename the pidfile\n            if self.pidfile is not None:\n                self.pidfile.rename(self.cfg.pidfile)\n            # reset proctitle\n            util._setproctitle(\"master [%s]\" % self.proc_name)\n\n    def wakeup(self):\n        \"\"\"Wake up the arbiter's main loop.\"\"\"\n        self.SIG_QUEUE.put_nowait(self.WAKEUP_REQUEST)\n\n    def halt(self, reason=None, exit_status=0):\n        \"\"\" halt arbiter \"\"\"\n        # Stop control socket server first\n        self._stop_control_server()\n\n        self.stop()\n\n        log_func = self.log.info if exit_status == 0 else self.log.error\n        log_func(\"Shutting down: %s\", self.master_name)\n        if reason is not None:\n            log_func(\"Reason: %s\", reason)\n\n        if self.pidfile is not None:\n            self.pidfile.unlink()\n        self.cfg.on_exit(self)\n        sys.exit(exit_status)\n\n    def wait_for_signals(self, timeout=1.0):\n        \"\"\"\\\n        Wait for signals with timeout.\n        Returns a list of signals that were received.\n        \"\"\"\n        signals = []\n        try:\n            # Block until we get a signal or timeout\n            sig = self.SIG_QUEUE.get(block=True, timeout=timeout)\n            if sig != self.WAKEUP_REQUEST:\n                signals.append(sig)\n            # Drain any additional queued signals\n            while True:\n                try:\n                    sig = self.SIG_QUEUE.get_nowait()\n                    if sig != self.WAKEUP_REQUEST:\n                        signals.append(sig)\n                except queue.Empty:\n                    break\n        except queue.Empty:\n            pass\n        except KeyboardInterrupt:\n            sys.exit()\n        return signals\n\n    def stop(self, graceful=True):\n        \"\"\"\\\n        Stop workers\n\n        :attr graceful: boolean, If True (the default) workers will be\n        killed gracefully  (ie. trying to wait for the current connection)\n        \"\"\"\n        unlink = (\n            self.reexec_pid == self.master_pid == 0\n            and not self.systemd\n            and not self.cfg.reuse_port\n        )\n        sock.close_sockets(self.LISTENERS, unlink)\n\n        self.LISTENERS = []\n        sig = signal.SIGTERM\n        if not graceful:\n            sig = signal.SIGQUIT\n        limit = time.time() + self.cfg.graceful_timeout\n\n        # Stop dirty arbiter\n        if self.dirty_arbiter_pid:\n            self.kill_dirty_arbiter(sig)\n\n        # instruct the workers to exit\n        self.kill_workers(sig)\n        # wait until the graceful timeout\n        quick_shutdown = not graceful\n        while (self.WORKERS or self.dirty_arbiter_pid) and time.time() < limit:\n            # Check for SIGINT/SIGQUIT to trigger quick shutdown\n            if not quick_shutdown:\n                try:\n                    pending_sig = self.SIG_QUEUE.get_nowait()\n                    if pending_sig in (signal.SIGINT, signal.SIGQUIT):\n                        self.log.info(\"Quick shutdown requested\")\n                        quick_shutdown = True\n                        self.kill_workers(signal.SIGQUIT)\n                        if self.dirty_arbiter_pid:\n                            self.kill_dirty_arbiter(signal.SIGQUIT)\n                        # Give workers a short time to exit cleanly\n                        limit = time.time() + 2.0\n                except Exception:\n                    pass\n            self.reap_workers()\n            self.reap_dirty_arbiter()\n            time.sleep(0.1)\n\n        self.kill_workers(signal.SIGKILL)\n        if self.dirty_arbiter_pid:\n            self.kill_dirty_arbiter(signal.SIGKILL)\n        # Final reap to clean up any remaining zombies\n        self.reap_workers()\n        self.reap_dirty_arbiter()\n\n    def reexec(self):\n        \"\"\"\\\n        Relaunch the master and workers.\n        \"\"\"\n        if self.reexec_pid != 0:\n            self.log.warning(\"USR2 signal ignored. Child exists.\")\n            return\n\n        if self.master_pid != 0:\n            self.log.warning(\"USR2 signal ignored. Parent exists.\")\n            return\n\n        master_pid = os.getpid()\n        self.reexec_pid = os.fork()\n        if self.reexec_pid != 0:\n            return\n\n        self.cfg.pre_exec(self)\n\n        environ = self.cfg.env_orig.copy()\n        environ['GUNICORN_PID'] = str(master_pid)\n\n        if self.systemd:\n            environ['LISTEN_PID'] = str(os.getpid())\n            environ['LISTEN_FDS'] = str(len(self.LISTENERS))\n        else:\n            environ['GUNICORN_FD'] = ','.join(\n                str(lnr.fileno()) for lnr in self.LISTENERS)\n\n        os.chdir(self.START_CTX['cwd'])\n\n        # exec the process using the original environment\n        os.execvpe(self.START_CTX[0], self.START_CTX['args'], environ)\n\n    def reload(self):\n        # Track reload stats\n        self._stats['reloads'] += 1\n\n        old_address = self.cfg.address\n\n        # reset old environment\n        for k in self.cfg.env:\n            if k in self.cfg.env_orig:\n                # reset the key to the value it had before\n                # we launched gunicorn\n                os.environ[k] = self.cfg.env_orig[k]\n            else:\n                # delete the value set by gunicorn\n                try:\n                    del os.environ[k]\n                except KeyError:\n                    pass\n\n        # reload conf\n        self.app.reload()\n        self.setup(self.app)\n\n        # reopen log files\n        self.log.reopen_files()\n\n        # do we need to change listener ?\n        if old_address != self.cfg.address:\n            # close all listeners\n            for lnr in self.LISTENERS:\n                lnr.close()\n            # init new listeners\n            self.LISTENERS = sock.create_sockets(self.cfg, self.log)\n            listeners_str = \",\".join([str(lnr) for lnr in self.LISTENERS])\n            self.log.info(\"Listening at: %s\", listeners_str)\n\n        # do some actions on reload\n        self.cfg.on_reload(self)\n\n        # unlink pidfile\n        if self.pidfile is not None:\n            self.pidfile.unlink()\n\n        # create new pidfile\n        if self.cfg.pidfile is not None:\n            self.pidfile = Pidfile(self.cfg.pidfile)\n            self.pidfile.create(self.pid)\n\n        # set new proc_name\n        util._setproctitle(\"master [%s]\" % self.proc_name)\n\n        # Remember current worker age before spawning new workers\n        last_worker_age = self.worker_age\n\n        # spawn new workers\n        for _ in range(self.cfg.workers):\n            self.spawn_worker()\n\n        # manage workers - this will kill old workers beyond num_workers\n        self.manage_workers()\n\n        # wait for old workers to terminate to prevent double SIGTERM\n        deadline = time.monotonic() + self.cfg.graceful_timeout\n        while time.monotonic() < deadline:\n            if not self.WORKERS:\n                break\n            # Check if all remaining workers are newer than last_worker_age\n            oldest = min(w.age for w in self.WORKERS.values())\n            if oldest > last_worker_age:\n                break\n            self.reap_workers()\n            time.sleep(0.1)\n\n    def murder_workers(self):\n        \"\"\"\\\n        Kill unused/idle workers\n        \"\"\"\n        if not self.timeout:\n            return\n        workers = list(self.WORKERS.items())\n        for (pid, worker) in workers:\n            try:\n                if time.monotonic() - worker.tmp.last_update() <= self.timeout:\n                    continue\n            except (OSError, ValueError):\n                continue\n\n            if not worker.aborted:\n                self.log.critical(\"WORKER TIMEOUT (pid:%s)\", pid)\n                worker.aborted = True\n                self.kill_worker(pid, signal.SIGABRT)\n            else:\n                self.kill_worker(pid, signal.SIGKILL)\n\n    def reap_workers(self):\n        \"\"\"\\\n        Reap workers to avoid zombie processes\n        \"\"\"\n        try:\n            while True:\n                wpid, status = os.waitpid(-1, os.WNOHANG)\n                if not wpid:\n                    break\n                if self.reexec_pid == wpid:\n                    self.reexec_pid = 0\n                else:\n                    # A worker was terminated. If the termination reason was\n                    # that it could not boot, we'll shut it down to avoid\n                    # infinite start/stop cycles.\n                    exitcode = None\n                    if os.WIFEXITED(status):\n                        exitcode = os.WEXITSTATUS(status)\n                    elif os.WIFSIGNALED(status):\n                        sig = os.WTERMSIG(status)\n                        try:\n                            sig_name = signal.Signals(sig).name\n                        except ValueError:\n                            sig_name = \"signal {}\".format(sig)\n                        msg = \"Worker (pid:{}) was sent {}!\".format(\n                            wpid, sig_name)\n\n                        # SIGKILL suggests OOM, log as error\n                        if sig == signal.SIGKILL:\n                            msg += \" Perhaps out of memory?\"\n                            self.log.error(msg)\n                        elif sig == signal.SIGTERM:\n                            # SIGTERM is expected during graceful shutdown\n                            self.log.info(msg)\n                        else:\n                            # Other signals are unexpected\n                            self.log.warning(msg)\n\n                    if exitcode is not None and exitcode != 0:\n                        self.log.error(\"Worker (pid:%s) exited with code %s.\",\n                                       wpid, exitcode)\n\n                    if exitcode == self.WORKER_BOOT_ERROR:\n                        reason = \"Worker failed to boot.\"\n                        raise HaltServer(reason, self.WORKER_BOOT_ERROR)\n                    if exitcode == self.APP_LOAD_ERROR:\n                        reason = \"App failed to load.\"\n                        raise HaltServer(reason, self.APP_LOAD_ERROR)\n\n                    worker = self.WORKERS.pop(wpid, None)\n                    if not worker:\n                        continue\n                    worker.tmp.close()\n                    self.cfg.child_exit(self, worker)\n        except OSError as e:\n            if e.errno != errno.ECHILD:\n                raise\n\n    def manage_workers(self):\n        \"\"\"\\\n        Maintain the number of workers by spawning or killing\n        as required.\n        \"\"\"\n        if len(self.WORKERS) < self.num_workers:\n            self.spawn_workers()\n\n        workers = self.WORKERS.items()\n        workers = sorted(workers, key=lambda w: w[1].age)\n        while len(workers) > self.num_workers:\n            (pid, _) = workers.pop(0)\n            self.kill_worker(pid, signal.SIGTERM)\n\n        active_worker_count = len(workers)\n        if self._last_logged_active_worker_count != active_worker_count:\n            self._last_logged_active_worker_count = active_worker_count\n            self.log.debug(\"{0} workers\".format(active_worker_count),\n                           extra={\"metric\": \"gunicorn.workers\",\n                                  \"value\": active_worker_count,\n                                  \"mtype\": \"gauge\"})\n\n        if self.cfg.enable_backlog_metric:\n            backlog = sum(sock.get_backlog() or 0\n                          for sock in self.LISTENERS)\n\n            if backlog >= 0:\n                self.log.debug(\"socket backlog: {0}\".format(backlog),\n                               extra={\"metric\": \"gunicorn.backlog\",\n                                      \"value\": backlog,\n                                      \"mtype\": \"histogram\"})\n\n    def spawn_worker(self):\n        self.worker_age += 1\n        worker = self.worker_class(self.worker_age, self.pid, self.LISTENERS,\n                                   self.app, self.timeout / 2.0,\n                                   self.cfg, self.log)\n        self.cfg.pre_fork(self, worker)\n\n        pid = os.fork()\n        if pid != 0:\n            worker.pid = pid\n            self.WORKERS[pid] = worker\n            self._stats['workers_spawned'] += 1\n            return pid\n\n        # Do not inherit the temporary files of other workers\n        for sibling in self.WORKERS.values():\n            sibling.tmp.close()\n\n        # Process Child\n        worker.pid = os.getpid()\n        try:\n            util._setproctitle(\"worker [%s]\" % self.proc_name)\n            self.log.info(\"Booting worker with pid: %s\", worker.pid)\n            if self.cfg.reuse_port:\n                worker.sockets = sock.create_sockets(self.cfg, self.log)\n            self.cfg.post_fork(self, worker)\n            worker.init_process()\n            sys.exit(0)\n        except SystemExit:\n            raise\n        except AppImportError as e:\n            self.log.debug(\"Exception while loading the application\",\n                           exc_info=True)\n            print(\"%s\" % e, file=sys.stderr)\n            sys.stderr.flush()\n            sys.exit(self.APP_LOAD_ERROR)\n        except Exception as e:\n            self.log.exception(\"Exception in worker process\")\n            print(\"%s\" % e, file=sys.stderr)\n            sys.stderr.flush()\n            if not worker.booted:\n                sys.exit(self.WORKER_BOOT_ERROR)\n            sys.exit(-1)\n        finally:\n            self.log.info(\"Worker exiting (pid: %s)\", worker.pid)\n            try:\n                worker.tmp.close()\n                self.cfg.worker_exit(self, worker)\n            except Exception:\n                self.log.warning(\"Exception during worker exit:\\n%s\",\n                                 traceback.format_exc())\n\n    def spawn_workers(self):\n        \"\"\"\\\n        Spawn new workers as needed.\n\n        This is where a worker process leaves the main loop\n        of the master process.\n        \"\"\"\n\n        for _ in range(self.num_workers - len(self.WORKERS)):\n            self.spawn_worker()\n            time.sleep(0.1 * random.random())\n\n    def kill_workers(self, sig):\n        \"\"\"\\\n        Kill all workers with the signal `sig`\n        :attr sig: `signal.SIG*` value\n        \"\"\"\n        worker_pids = list(self.WORKERS.keys())\n        for pid in worker_pids:\n            self.kill_worker(pid, sig)\n\n    def kill_worker(self, pid, sig):\n        \"\"\"\\\n        Kill a worker\n\n        :attr pid: int, worker pid\n        :attr sig: `signal.SIG*` value\n         \"\"\"\n        try:\n            os.kill(pid, sig)\n            # Track kills only on SIGTERM/SIGKILL (actual termination signals)\n            if sig in (signal.SIGTERM, signal.SIGKILL):\n                self._stats['workers_killed'] += 1\n        except OSError as e:\n            if e.errno == errno.ESRCH:\n                try:\n                    worker = self.WORKERS.pop(pid)\n                    worker.tmp.close()\n                    self.cfg.worker_exit(self, worker)\n                    return\n                except (KeyError, OSError):\n                    return\n            raise\n\n    # =========================================================================\n    # Dirty Arbiter Management\n    # =========================================================================\n\n    def _get_dirty_pidfile_path(self):\n        \"\"\"Get the well-known PID file path for orphan detection.\n\n        Uses self.proc_name (not self.cfg.proc_name) so that during USR2\n        the new master gets a different PID file path (\"myapp.2\" vs \"myapp\").\n        This prevents the old dirty arbiter from removing the new one's PID file.\n        \"\"\"\n        import tempfile\n        safe_name = self.proc_name.replace('/', '_').replace(' ', '_')\n        return os.path.join(tempfile.gettempdir(), f\"gunicorn-dirty-{safe_name}.pid\")\n\n    def _cleanup_orphaned_dirty_arbiter(self):\n        \"\"\"Kill any orphaned dirty arbiter from a previous crash.\n\n        Only runs on fresh start (master_pid == 0), not during USR2.\n        \"\"\"\n        # During USR2, master_pid is set - don't cleanup old dirty arbiter\n        if self.master_pid != 0:\n            return\n\n        pidfile = self._get_dirty_pidfile_path()\n        if not os.path.exists(pidfile):\n            return\n\n        try:\n            with open(pidfile) as f:\n                old_pid = int(f.read().strip())\n\n            # Check if process exists\n            os.kill(old_pid, 0)\n            # Process exists - kill orphan\n            self.log.warning(\"Killing orphaned dirty arbiter (pid: %s)\", old_pid)\n            os.kill(old_pid, signal.SIGTERM)\n            # Wait briefly for graceful exit\n            for _ in range(10):\n                time.sleep(0.1)\n                try:\n                    os.kill(old_pid, 0)\n                except OSError:\n                    break\n            else:\n                os.kill(old_pid, signal.SIGKILL)\n        except (ValueError, IOError, OSError):\n            pass\n\n        # Remove stale PID file\n        try:\n            os.unlink(pidfile)\n        except OSError:\n            pass\n\n    def spawn_dirty_arbiter(self):\n        \"\"\"\\\n        Spawn the dirty arbiter process.\n\n        The dirty arbiter manages a separate pool of workers for\n        long-running, blocking operations.\n        \"\"\"\n        # Lazy import for gevent compatibility (see #3482)\n        from gunicorn.dirty import DirtyArbiter, set_dirty_socket_path\n\n        if self.dirty_arbiter_pid:\n            return  # Already running\n\n        # Cleanup any orphaned dirty arbiter from previous crash\n        self._cleanup_orphaned_dirty_arbiter()\n\n        # Get well-known PID file path\n        self.dirty_pidfile = self._get_dirty_pidfile_path()\n\n        self.dirty_arbiter = DirtyArbiter(\n            self.cfg, self.log,\n            pidfile=self.dirty_pidfile\n        )\n        socket_path = self.dirty_arbiter.socket_path\n\n        pid = os.fork()\n        if pid != 0:\n            # Parent process\n            self.dirty_arbiter_pid = pid\n            # Set socket path for HTTP workers to use\n            set_dirty_socket_path(socket_path)\n            os.environ['GUNICORN_DIRTY_SOCKET'] = socket_path\n            self.log.info(\"Spawned dirty arbiter (pid: %s) at %s\",\n                          pid, socket_path)\n            return pid\n\n        # Child process - run the dirty arbiter\n        try:\n            self.dirty_arbiter.run()\n            sys.exit(0)\n        except SystemExit:\n            raise\n        except Exception:\n            self.log.exception(\"Exception in dirty arbiter process\")\n            sys.exit(-1)\n\n    def kill_dirty_arbiter(self, sig):\n        \"\"\"\\\n        Send a signal to the dirty arbiter.\n\n        :attr sig: `signal.SIG*` value\n        \"\"\"\n        if not self.dirty_arbiter_pid:\n            return\n\n        try:\n            os.kill(self.dirty_arbiter_pid, sig)\n        except OSError as e:\n            if e.errno == errno.ESRCH:\n                self.dirty_arbiter_pid = 0\n                self.dirty_arbiter = None\n\n    def reap_dirty_arbiter(self):\n        \"\"\"\\\n        Reap the dirty arbiter process if it has exited.\n        \"\"\"\n        if not self.dirty_arbiter_pid:\n            return\n\n        try:\n            wpid, status = os.waitpid(self.dirty_arbiter_pid, os.WNOHANG)\n            if not wpid:\n                return\n\n            if os.WIFEXITED(status):\n                exitcode = os.WEXITSTATUS(status)\n                if exitcode != 0:\n                    self.log.error(\"Dirty arbiter (pid:%s) exited with code %s\",\n                                   wpid, exitcode)\n                else:\n                    self.log.info(\"Dirty arbiter (pid:%s) exited\", wpid)\n            elif os.WIFSIGNALED(status):\n                sig = os.WTERMSIG(status)\n                self.log.warning(\"Dirty arbiter (pid:%s) killed by signal %s\",\n                                 wpid, sig)\n\n            self.dirty_arbiter_pid = 0\n            self.dirty_arbiter = None\n        except OSError as e:\n            if e.errno == errno.ECHILD:\n                self.dirty_arbiter_pid = 0\n                self.dirty_arbiter = None\n\n    def manage_dirty_arbiter(self):\n        \"\"\"\\\n        Maintain the dirty arbiter process by respawning if needed.\n        \"\"\"\n        if self.dirty_arbiter_pid:\n            return  # Already running\n\n        if self.cfg.dirty_workers > 0 and self.cfg.dirty_apps:\n            self.log.info(\"Spawning dirty arbiter...\")\n            self.spawn_dirty_arbiter()\n\n    # =========================================================================\n    # Control Socket Management\n    # =========================================================================\n\n    def _get_control_socket_path(self):\n        \"\"\"Get the control socket path, making relative paths absolute.\"\"\"\n        socket_path = self.cfg.control_socket\n        if not os.path.isabs(socket_path):\n            socket_path = os.path.join(util.getcwd(), socket_path)\n        return socket_path\n\n    def _start_control_server(self):\n        \"\"\"\\\n        Start the control socket server.\n\n        The server runs in a background thread and accepts commands\n        via Unix socket.\n        \"\"\"\n        if self.cfg.control_socket_disable:\n            self.log.debug(\"Control socket disabled\")\n            return\n\n        # Lazy import to avoid circular imports and gevent compatibility\n        from gunicorn.ctl.server import ControlSocketServer\n\n        socket_path = self._get_control_socket_path()\n        socket_mode = self.cfg.control_socket_mode\n\n        try:\n            self._control_server = ControlSocketServer(\n                self, socket_path, socket_mode\n            )\n            self._control_server.start()\n        except Exception as e:\n            self.log.warning(\"Failed to start control socket: %s\", e)\n            self._control_server = None\n\n    def _stop_control_server(self):\n        \"\"\"\\\n        Stop the control socket server.\n        \"\"\"\n        if self._control_server:\n            try:\n                self._control_server.stop()\n            except Exception as e:\n                self.log.debug(\"Error stopping control server: %s\", e)\n            self._control_server = None\n"
  },
  {
    "path": "gunicorn/asgi/__init__.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nASGI support for gunicorn.\n\nThis module provides native ASGI worker support, using gunicorn's own\nHTTP parsing infrastructure adapted for async I/O.\n\nComponents:\n- AsyncUnreader: Async socket reading with pushback buffer\n- AsyncRequest: Async HTTP request parser\n- ASGIProtocol: asyncio.Protocol implementation for HTTP handling\n- WebSocketProtocol: WebSocket protocol handler (RFC 6455)\n- LifespanManager: ASGI lifespan protocol support\n\nUsage:\n    gunicorn -k asgi myapp:app\n\"\"\"\n\nfrom gunicorn.asgi.unreader import AsyncUnreader\nfrom gunicorn.asgi.message import AsyncRequest\nfrom gunicorn.asgi.lifespan import LifespanManager\n\n__all__ = ['AsyncUnreader', 'AsyncRequest', 'LifespanManager']\n"
  },
  {
    "path": "gunicorn/asgi/lifespan.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nASGI lifespan protocol manager.\n\nManages startup and shutdown events for ASGI applications,\nenabling frameworks like FastAPI to run initialization and\ncleanup code.\n\"\"\"\n\nimport asyncio\n\n\nclass LifespanManager:\n    \"\"\"Manages ASGI lifespan events (startup/shutdown).\n\n    The lifespan protocol allows ASGI applications to run code at\n    startup and shutdown. This is essential for applications that\n    need to initialize database connections, caches, or other\n    resources.\n\n    ASGI lifespan messages:\n    - Server sends: {\"type\": \"lifespan.startup\"}\n    - App responds: {\"type\": \"lifespan.startup.complete\"} or\n                    {\"type\": \"lifespan.startup.failed\", \"message\": \"...\"}\n    - Server sends: {\"type\": \"lifespan.shutdown\"}\n    - App responds: {\"type\": \"lifespan.shutdown.complete\"}\n    \"\"\"\n\n    def __init__(self, app, logger, state=None):\n        \"\"\"Initialize the lifespan manager.\n\n        Args:\n            app: ASGI application callable\n            logger: Logger instance\n            state: Shared state dict for the application\n        \"\"\"\n        self.app = app\n        self.logger = logger\n        self.state = state if state is not None else {}\n\n        self._startup_complete = asyncio.Event()\n        self._shutdown_complete = asyncio.Event()\n        self._startup_failed = False\n        self._startup_error = None\n        self._shutdown_error = None\n        self._receive_queue = asyncio.Queue()\n        self._task = None\n        self._app_finished = False\n\n    async def startup(self):\n        \"\"\"Run lifespan startup and wait for completion.\n\n        Raises:\n            RuntimeError: If startup fails or app doesn't support lifespan\n        \"\"\"\n        scope = {\n            \"type\": \"lifespan\",\n            \"asgi\": {\"version\": \"3.0\", \"spec_version\": \"2.4\"},\n            \"state\": self.state,\n        }\n\n        # Send startup event\n        await self._receive_queue.put({\"type\": \"lifespan.startup\"})\n\n        # Run lifespan in background task\n        self._task = asyncio.create_task(self._run_lifespan(scope))\n\n        # Wait for startup with timeout\n        try:\n            await asyncio.wait_for(\n                self._startup_complete.wait(),\n                timeout=30.0  # Reasonable startup timeout\n            )\n        except asyncio.TimeoutError:\n            if self._task:\n                self._task.cancel()\n            raise RuntimeError(\"Lifespan startup timed out\")\n\n        if self._startup_failed:\n            if self._task:\n                self._task.cancel()\n            msg = self._startup_error or \"Unknown error\"\n            raise RuntimeError(f\"Lifespan startup failed: {msg}\")\n\n        self.logger.debug(\"ASGI lifespan startup complete\")\n\n    async def shutdown(self):\n        \"\"\"Signal shutdown and wait for completion.\n\n        This should be called during graceful shutdown.\n        \"\"\"\n        if self._app_finished:\n            self.logger.debug(\"ASGI lifespan already finished\")\n            return\n\n        # Send shutdown event\n        await self._receive_queue.put({\"type\": \"lifespan.shutdown\"})\n\n        # Wait for shutdown with timeout\n        try:\n            await asyncio.wait_for(\n                self._shutdown_complete.wait(),\n                timeout=30.0  # Reasonable shutdown timeout\n            )\n        except asyncio.TimeoutError:\n            self.logger.warning(\"Lifespan shutdown timed out\")\n\n        if self._shutdown_error:\n            self.logger.error(\"Lifespan shutdown error: %s\", self._shutdown_error)\n\n        # Cancel the task if still running\n        if self._task and not self._task.done():\n            self._task.cancel()\n            try:\n                await self._task\n            except asyncio.CancelledError:\n                pass\n\n        self.logger.debug(\"ASGI lifespan shutdown complete\")\n\n    async def _run_lifespan(self, scope):\n        \"\"\"Run the ASGI lifespan protocol.\"\"\"\n        try:\n            await self.app(scope, self._receive, self._send)\n        except asyncio.CancelledError:\n            raise\n        except Exception as e:\n            self.logger.debug(\"Lifespan application raised: %s\", e)\n            # If startup hasn't completed, mark it as failed\n            if not self._startup_complete.is_set():\n                self._startup_failed = True\n                self._startup_error = str(e)\n                self._startup_complete.set()\n            # If shutdown hasn't completed, mark error\n            elif not self._shutdown_complete.is_set():\n                self._shutdown_error = str(e)\n                self._shutdown_complete.set()\n        finally:\n            self._app_finished = True\n            # Ensure events are set to unblock waiters\n            if not self._startup_complete.is_set():\n                self._startup_failed = True\n                self._startup_error = \"Application exited before startup complete\"\n                self._startup_complete.set()\n            if not self._shutdown_complete.is_set():\n                self._shutdown_complete.set()\n\n    async def _receive(self):\n        \"\"\"ASGI receive callable for lifespan.\"\"\"\n        return await self._receive_queue.get()\n\n    async def _send(self, message):\n        \"\"\"ASGI send callable for lifespan.\"\"\"\n        msg_type = message[\"type\"]\n\n        if msg_type == \"lifespan.startup.complete\":\n            self._startup_complete.set()\n            self.logger.debug(\"Received lifespan.startup.complete\")\n\n        elif msg_type == \"lifespan.startup.failed\":\n            self._startup_failed = True\n            self._startup_error = message.get(\"message\", \"\")\n            self._startup_complete.set()\n            self.logger.debug(\"Received lifespan.startup.failed: %s\",\n                              self._startup_error)\n\n        elif msg_type == \"lifespan.shutdown.complete\":\n            self._shutdown_complete.set()\n            self.logger.debug(\"Received lifespan.shutdown.complete\")\n\n        elif msg_type == \"lifespan.shutdown.failed\":\n            self._shutdown_error = message.get(\"message\", \"\")\n            self._shutdown_complete.set()\n            self.logger.debug(\"Received lifespan.shutdown.failed: %s\",\n                              self._shutdown_error)\n"
  },
  {
    "path": "gunicorn/asgi/message.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nAsync version of gunicorn/http/message.py for ASGI workers.\n\nReuses the parsing logic from the sync version, adapted for async I/O.\n\"\"\"\n\nimport ipaddress\nimport re\nimport socket\nimport struct\n\nfrom gunicorn.http.errors import (\n    ExpectationFailed,\n    InvalidHeader, InvalidHeaderName, NoMoreData,\n    InvalidRequestLine, InvalidRequestMethod, InvalidHTTPVersion,\n    LimitRequestLine, LimitRequestHeaders,\n    UnsupportedTransferCoding, ObsoleteFolding,\n    InvalidProxyLine, InvalidProxyHeader, ForbiddenProxyRequest,\n    InvalidSchemeHeaders,\n)\nfrom gunicorn.http.message import (\n    PP_V2_SIGNATURE, PPCommand, PPFamily, PPProtocol\n)\nfrom gunicorn.util import bytes_to_str, split_request_uri\n\nMAX_REQUEST_LINE = 8190\nMAX_HEADERS = 32768\nDEFAULT_MAX_HEADERFIELD_SIZE = 8190\n\n# Reuse regex patterns from sync version\nRFC9110_5_6_2_TOKEN_SPECIALS = r\"!#$%&'*+-.^_`|~\"\nTOKEN_RE = re.compile(r\"[%s0-9a-zA-Z]+\" % (re.escape(RFC9110_5_6_2_TOKEN_SPECIALS)))\nMETHOD_BADCHAR_RE = re.compile(\"[a-z#]\")\nVERSION_RE = re.compile(r\"HTTP/(\\d)\\.(\\d)\")\nRFC9110_5_5_INVALID_AND_DANGEROUS = re.compile(r\"[\\0\\r\\n]\")\n\n\ndef _ip_in_allow_list(ip_str, allow_list, networks):\n    \"\"\"Check if IP address is in the allow list.\n\n    Args:\n        ip_str: The IP address string to check\n        allow_list: The original allow list (strings, may contain \"*\")\n        networks: Pre-computed ipaddress.ip_network objects from config\n    \"\"\"\n    if '*' in allow_list:\n        return True\n    try:\n        ip = ipaddress.ip_address(ip_str)\n    except ValueError:\n        return False\n    for network in networks:\n        if ip in network:\n            return True\n    return False\n\n\nclass AsyncRequest:\n    \"\"\"Async HTTP request parser.\n\n    Parses HTTP/1.x requests using async I/O, reusing gunicorn's\n    parsing logic where possible.\n    \"\"\"\n\n    def __init__(self, cfg, unreader, peer_addr, req_number=1):\n        self.cfg = cfg\n        self.unreader = unreader\n        self.peer_addr = peer_addr\n        self.remote_addr = peer_addr\n        self.req_number = req_number\n\n        self.version = None\n        self.method = None\n        self.uri = None\n        self.path = None\n        self.query = None\n        self.fragment = None\n        self.headers = []\n        self.trailers = []\n        self.scheme = \"https\" if cfg.is_ssl else \"http\"\n        self.must_close = False\n        self._expected_100_continue = False\n\n        self.proxy_protocol_info = None\n\n        # Request line limit\n        self.limit_request_line = cfg.limit_request_line\n        if (self.limit_request_line < 0\n                or self.limit_request_line >= MAX_REQUEST_LINE):\n            self.limit_request_line = MAX_REQUEST_LINE\n\n        # Headers limits\n        self.limit_request_fields = cfg.limit_request_fields\n        if (self.limit_request_fields <= 0\n                or self.limit_request_fields > MAX_HEADERS):\n            self.limit_request_fields = MAX_HEADERS\n\n        self.limit_request_field_size = cfg.limit_request_field_size\n        if self.limit_request_field_size < 0:\n            self.limit_request_field_size = DEFAULT_MAX_HEADERFIELD_SIZE\n\n        # Max header buffer size\n        max_header_field_size = self.limit_request_field_size or DEFAULT_MAX_HEADERFIELD_SIZE\n        self.max_buffer_headers = self.limit_request_fields * \\\n            (max_header_field_size + 2) + 4\n\n        # Body-related state\n        self.content_length = None\n        self.chunked = False\n        self._body_reader = None\n        self._body_remaining = 0\n\n    @classmethod\n    async def parse(cls, cfg, unreader, peer_addr, req_number=1):\n        \"\"\"Parse an HTTP request from the stream.\n\n        Args:\n            cfg: gunicorn config object\n            unreader: AsyncUnreader instance\n            peer_addr: client address tuple\n            req_number: request number on this connection (for keepalive)\n\n        Returns:\n            AsyncRequest: Parsed request object\n\n        Raises:\n            NoMoreData: If no data available\n            Various parsing errors for malformed requests\n        \"\"\"\n        req = cls(cfg, unreader, peer_addr, req_number)\n        await req._parse()\n        return req\n\n    async def _parse(self):\n        \"\"\"Parse the request from the unreader.\"\"\"\n        buf = bytearray()\n        await self._read_into(buf)\n\n        # Handle proxy protocol if enabled and this is the first request\n        mode = self.cfg.proxy_protocol\n        if mode != \"off\" and self.req_number == 1:\n            buf = await self._handle_proxy_protocol(buf, mode)\n\n        # Get request line\n        line, buf = await self._read_line(buf, self.limit_request_line)\n\n        self._parse_request_line(line)\n\n        # Headers - use bytearray.find() directly to avoid bytes() conversions\n        while True:\n            idx = buf.find(b\"\\r\\n\\r\\n\")\n            done = buf[:2] == b\"\\r\\n\"\n\n            if idx < 0 and not done:\n                await self._read_into(buf)\n                if len(buf) > self.max_buffer_headers:\n                    raise LimitRequestHeaders(\"max buffer headers\")\n            else:\n                break\n\n        if done:\n            self.unreader.unread(bytes(buf[2:]))\n        else:\n            self.headers = self._parse_headers(bytes(buf[:idx]), from_trailer=False)\n            self.unreader.unread(bytes(buf[idx + 4:]))\n\n        self._set_body_reader()\n\n    async def _read_into(self, buf):\n        \"\"\"Read data from unreader and append to bytearray buffer.\"\"\"\n        data = await self.unreader.read()\n        if not data:\n            raise NoMoreData(bytes(buf))\n        buf.extend(data)\n\n    async def _read_line(self, buf, limit=0):\n        \"\"\"Read a line from buffer, returning (line, remaining_buffer).\n\n        Uses bytearray.find() directly to avoid repeated bytes() conversions.\n        \"\"\"\n        while True:\n            idx = buf.find(b\"\\r\\n\")\n            if idx >= 0:\n                if idx > limit > 0:\n                    raise LimitRequestLine(idx, limit)\n                break\n            if len(buf) - 2 > limit > 0:\n                raise LimitRequestLine(len(buf), limit)\n            await self._read_into(buf)\n\n        line = bytes(buf[:idx])\n        remaining = bytearray(buf[idx + 2:])\n        return (line, remaining)\n\n    async def _handle_proxy_protocol(self, buf, mode):\n        \"\"\"Handle PROXY protocol detection and parsing.\n\n        Returns the buffer with proxy protocol data consumed.\n        \"\"\"\n        # Ensure we have enough data to detect v2 signature (12 bytes)\n        while len(buf) < 12:\n            await self._read_into(buf)\n\n        # Check for v2 signature first\n        if mode in (\"v2\", \"auto\") and buf[:12] == PP_V2_SIGNATURE:\n            self._proxy_protocol_access_check()\n            return await self._parse_proxy_protocol_v2(buf)\n\n        # Check for v1 prefix\n        if mode in (\"v1\", \"auto\") and buf[:6] == b\"PROXY \":\n            self._proxy_protocol_access_check()\n            return await self._parse_proxy_protocol_v1(buf)\n\n        # Not proxy protocol - return buffer unchanged\n        return buf\n\n    def _proxy_protocol_access_check(self):\n        \"\"\"Check if proxy protocol is allowed from this peer.\"\"\"\n        if (isinstance(self.peer_addr, tuple) and\n                not _ip_in_allow_list(self.peer_addr[0], self.cfg.proxy_allow_ips,\n                                      self.cfg.proxy_allow_networks())):\n            raise ForbiddenProxyRequest(self.peer_addr[0])\n\n    async def _parse_proxy_protocol_v1(self, buf):\n        \"\"\"Parse PROXY protocol v1 (text format).\n\n        Returns buffer with v1 header consumed.\n        \"\"\"\n        # Read until we find \\r\\n\n        data = bytes(buf)\n        while b\"\\r\\n\" not in data:\n            await self._read_into(buf)\n            data = bytes(buf)\n\n        idx = data.find(b\"\\r\\n\")\n        line = bytes_to_str(data[:idx])\n        remaining = bytearray(data[idx + 2:])\n\n        bits = line.split(\" \")\n\n        if len(bits) != 6:\n            raise InvalidProxyLine(line)\n\n        proto = bits[1]\n        s_addr = bits[2]\n        d_addr = bits[3]\n\n        if proto not in [\"TCP4\", \"TCP6\"]:\n            raise InvalidProxyLine(\"protocol '%s' not supported\" % proto)\n\n        if proto == \"TCP4\":\n            try:\n                socket.inet_pton(socket.AF_INET, s_addr)\n                socket.inet_pton(socket.AF_INET, d_addr)\n            except OSError:\n                raise InvalidProxyLine(line)\n        elif proto == \"TCP6\":\n            try:\n                socket.inet_pton(socket.AF_INET6, s_addr)\n                socket.inet_pton(socket.AF_INET6, d_addr)\n            except OSError:\n                raise InvalidProxyLine(line)\n\n        try:\n            s_port = int(bits[4])\n            d_port = int(bits[5])\n        except ValueError:\n            raise InvalidProxyLine(\"invalid port %s\" % line)\n\n        if not ((0 <= s_port <= 65535) and (0 <= d_port <= 65535)):\n            raise InvalidProxyLine(\"invalid port %s\" % line)\n\n        self.proxy_protocol_info = {\n            \"proxy_protocol\": proto,\n            \"client_addr\": s_addr,\n            \"client_port\": s_port,\n            \"proxy_addr\": d_addr,\n            \"proxy_port\": d_port\n        }\n\n        return remaining\n\n    async def _parse_proxy_protocol_v2(self, buf):\n        \"\"\"Parse PROXY protocol v2 (binary format).\n\n        Returns buffer with v2 header consumed.\n        \"\"\"\n        # We need at least 16 bytes for the header (12 signature + 4 header)\n        while len(buf) < 16:\n            await self._read_into(buf)\n\n        # Parse header fields (after 12-byte signature)\n        ver_cmd = buf[12]\n        fam_proto = buf[13]\n        length = struct.unpack(\">H\", bytes(buf[14:16]))[0]\n\n        # Validate version (high nibble must be 0x2)\n        version = (ver_cmd & 0xF0) >> 4\n        if version != 2:\n            raise InvalidProxyHeader(\"unsupported version %d\" % version)\n\n        # Extract command (low nibble)\n        command = ver_cmd & 0x0F\n        if command not in (PPCommand.LOCAL, PPCommand.PROXY):\n            raise InvalidProxyHeader(\"unsupported command %d\" % command)\n\n        # Ensure we have the complete header\n        total_header_size = 16 + length\n        while len(buf) < total_header_size:\n            await self._read_into(buf)\n\n        # For LOCAL command, no address info is provided\n        if command == PPCommand.LOCAL:\n            self.proxy_protocol_info = {\n                \"proxy_protocol\": \"LOCAL\",\n                \"client_addr\": None,\n                \"client_port\": None,\n                \"proxy_addr\": None,\n                \"proxy_port\": None\n            }\n            return bytearray(buf[total_header_size:])\n\n        # Extract address family and protocol\n        family = (fam_proto & 0xF0) >> 4\n        protocol = fam_proto & 0x0F\n\n        # We only support TCP (STREAM)\n        if protocol != PPProtocol.STREAM:\n            raise InvalidProxyHeader(\"only TCP protocol is supported\")\n\n        addr_data = bytes(buf[16:16 + length])\n\n        if family == PPFamily.INET:  # IPv4\n            if length < 12:  # 4+4+2+2\n                raise InvalidProxyHeader(\"insufficient address data for IPv4\")\n            s_addr = socket.inet_ntop(socket.AF_INET, addr_data[0:4])\n            d_addr = socket.inet_ntop(socket.AF_INET, addr_data[4:8])\n            s_port = struct.unpack(\">H\", addr_data[8:10])[0]\n            d_port = struct.unpack(\">H\", addr_data[10:12])[0]\n            proto = \"TCP4\"\n\n        elif family == PPFamily.INET6:  # IPv6\n            if length < 36:  # 16+16+2+2\n                raise InvalidProxyHeader(\"insufficient address data for IPv6\")\n            s_addr = socket.inet_ntop(socket.AF_INET6, addr_data[0:16])\n            d_addr = socket.inet_ntop(socket.AF_INET6, addr_data[16:32])\n            s_port = struct.unpack(\">H\", addr_data[32:34])[0]\n            d_port = struct.unpack(\">H\", addr_data[34:36])[0]\n            proto = \"TCP6\"\n\n        elif family == PPFamily.UNSPEC:\n            # No address info provided with PROXY command\n            self.proxy_protocol_info = {\n                \"proxy_protocol\": \"UNSPEC\",\n                \"client_addr\": None,\n                \"client_port\": None,\n                \"proxy_addr\": None,\n                \"proxy_port\": None\n            }\n            return bytearray(buf[total_header_size:])\n\n        else:\n            raise InvalidProxyHeader(\"unsupported address family %d\" % family)\n\n        # Set data\n        self.proxy_protocol_info = {\n            \"proxy_protocol\": proto,\n            \"client_addr\": s_addr,\n            \"client_port\": s_port,\n            \"proxy_addr\": d_addr,\n            \"proxy_port\": d_port\n        }\n\n        return bytearray(buf[total_header_size:])\n\n    def _parse_request_line(self, line_bytes):\n        \"\"\"Parse the HTTP request line.\"\"\"\n        bits = [bytes_to_str(bit) for bit in line_bytes.split(b\" \", 2)]\n        if len(bits) != 3:\n            raise InvalidRequestLine(bytes_to_str(line_bytes))\n\n        # Method\n        self.method = bits[0]\n\n        if not self.cfg.permit_unconventional_http_method:\n            if METHOD_BADCHAR_RE.search(self.method):\n                raise InvalidRequestMethod(self.method)\n            if not 3 <= len(bits[0]) <= 20:\n                raise InvalidRequestMethod(self.method)\n        if not TOKEN_RE.fullmatch(self.method):\n            raise InvalidRequestMethod(self.method)\n        if self.cfg.casefold_http_method:\n            self.method = self.method.upper()\n\n        # URI\n        self.uri = bits[1]\n\n        if len(self.uri) == 0:\n            raise InvalidRequestLine(bytes_to_str(line_bytes))\n\n        try:\n            parts = split_request_uri(self.uri)\n        except ValueError:\n            raise InvalidRequestLine(bytes_to_str(line_bytes))\n        self.path = parts.path or \"\"\n        self.query = parts.query or \"\"\n        self.fragment = parts.fragment or \"\"\n\n        # Version\n        match = VERSION_RE.fullmatch(bits[2])\n        if match is None:\n            raise InvalidHTTPVersion(bits[2])\n        self.version = (int(match.group(1)), int(match.group(2)))\n        if not (1, 0) <= self.version < (2, 0):\n            if not self.cfg.permit_unconventional_http_version:\n                raise InvalidHTTPVersion(self.version)\n\n    def _parse_headers(self, data, from_trailer=False):\n        \"\"\"Parse HTTP headers from raw data.\n\n        Uses index-based iteration instead of list.pop(0) for O(1) access.\n        \"\"\"\n        cfg = self.cfg\n        headers = []\n\n        lines = [bytes_to_str(line) for line in data.split(b\"\\r\\n\")]\n        num_lines = len(lines)\n        i = 0\n\n        # Handle scheme headers\n        scheme_header = False\n        secure_scheme_headers = {}\n        forwarder_headers = []\n        if from_trailer:\n            pass\n        elif (not isinstance(self.peer_addr, tuple)\n              or _ip_in_allow_list(self.peer_addr[0], cfg.forwarded_allow_ips,\n                                   cfg.forwarded_allow_networks())):\n            secure_scheme_headers = cfg.secure_scheme_headers\n            forwarder_headers = cfg.forwarder_headers\n\n        while i < num_lines:\n            if len(headers) >= self.limit_request_fields:\n                raise LimitRequestHeaders(\"limit request headers fields\")\n\n            curr = lines[i]\n            i += 1\n            header_length = len(curr) + len(\"\\r\\n\")\n            if curr.find(\":\") <= 0:\n                raise InvalidHeader(curr)\n            name, value = curr.split(\":\", 1)\n            if self.cfg.strip_header_spaces:\n                name = name.rstrip(\" \\t\")\n            if not TOKEN_RE.fullmatch(name):\n                raise InvalidHeaderName(name)\n\n            name = name.upper()\n            value = [value.strip(\" \\t\")]\n\n            # Consume value continuation lines using index-based iteration\n            while i < num_lines and lines[i].startswith((\" \", \"\\t\")):\n                if not self.cfg.permit_obsolete_folding:\n                    raise ObsoleteFolding(name)\n                curr = lines[i]\n                i += 1\n                header_length += len(curr) + len(\"\\r\\n\")\n                if header_length > self.limit_request_field_size > 0:\n                    raise LimitRequestHeaders(\"limit request headers fields size\")\n                value.append(curr.strip(\"\\t \"))\n            value = \" \".join(value)\n\n            if RFC9110_5_5_INVALID_AND_DANGEROUS.search(value):\n                raise InvalidHeader(name)\n\n            if header_length > self.limit_request_field_size > 0:\n                raise LimitRequestHeaders(\"limit request headers fields size\")\n\n            if not from_trailer and name == \"EXPECT\":\n                # https://datatracker.ietf.org/doc/html/rfc9110#section-10.1.1\n                # \"The Expect field value is case-insensitive.\"\n                if value.lower() == \"100-continue\":\n                    if self.version < (1, 1):\n                        # https://datatracker.ietf.org/doc/html/rfc9110#section-10.1.1-12\n                        # \"A server that receives a 100-continue expectation\n                        #  in an HTTP/1.0 request MUST ignore that expectation.\"\n                        pass\n                    else:\n                        self._expected_100_continue = True\n                    # N.B. understood but ignored expect header does not return 417\n                else:\n                    raise ExpectationFailed(value)\n\n            if name in secure_scheme_headers:\n                secure = value == secure_scheme_headers[name]\n                scheme = \"https\" if secure else \"http\"\n                if scheme_header:\n                    if scheme != self.scheme:\n                        raise InvalidSchemeHeaders()\n                else:\n                    scheme_header = True\n                    self.scheme = scheme\n\n            if \"_\" in name:\n                if name in forwarder_headers or \"*\" in forwarder_headers:\n                    pass\n                elif self.cfg.header_map == \"dangerous\":\n                    pass\n                elif self.cfg.header_map == \"drop\":\n                    continue\n                else:\n                    raise InvalidHeaderName(name)\n\n            headers.append((name, value))\n\n        return headers\n\n    def _set_body_reader(self):\n        \"\"\"Determine how to read the request body.\"\"\"\n        chunked = False\n        content_length = None\n\n        for (name, value) in self.headers:\n            if name == \"CONTENT-LENGTH\":\n                if content_length is not None:\n                    raise InvalidHeader(\"CONTENT-LENGTH\", req=self)\n                content_length = value\n            elif name == \"TRANSFER-ENCODING\":\n                vals = [v.strip() for v in value.split(',')]\n                for val in vals:\n                    if val.lower() == \"chunked\":\n                        if chunked:\n                            raise InvalidHeader(\"TRANSFER-ENCODING\", req=self)\n                        chunked = True\n                    elif val.lower() == \"identity\":\n                        if chunked:\n                            raise InvalidHeader(\"TRANSFER-ENCODING\", req=self)\n                    elif val.lower() in ('compress', 'deflate', 'gzip'):\n                        if chunked:\n                            raise InvalidHeader(\"TRANSFER-ENCODING\", req=self)\n                        self.force_close()\n                    else:\n                        raise UnsupportedTransferCoding(value)\n\n        if chunked:\n            if self.version < (1, 1):\n                raise InvalidHeader(\"TRANSFER-ENCODING\", req=self)\n            if content_length is not None:\n                raise InvalidHeader(\"CONTENT-LENGTH\", req=self)\n            self.chunked = True\n            self.content_length = None\n            self._body_remaining = -1\n        elif content_length is not None:\n            try:\n                if str(content_length).isnumeric():\n                    content_length = int(content_length)\n                else:\n                    raise InvalidHeader(\"CONTENT-LENGTH\", req=self)\n            except ValueError:\n                raise InvalidHeader(\"CONTENT-LENGTH\", req=self)\n\n            if content_length < 0:\n                raise InvalidHeader(\"CONTENT-LENGTH\", req=self)\n\n            self.content_length = content_length\n            self._body_remaining = content_length\n        else:\n            # No body for requests without Content-Length or Transfer-Encoding\n            self.content_length = 0\n            self._body_remaining = 0\n\n    def force_close(self):\n        \"\"\"Mark connection for closing after this request.\"\"\"\n        self.must_close = True\n\n    def should_close(self):\n        \"\"\"Check if connection should be closed after this request.\"\"\"\n        if self.must_close:\n            return True\n        for (h, v) in self.headers:\n            if h == \"CONNECTION\":\n                v = v.lower().strip(\" \\t\")\n                if v == \"close\":\n                    return True\n                elif v == \"keep-alive\":\n                    return False\n                break\n        return self.version <= (1, 0)\n\n    def get_header(self, name):\n        \"\"\"Get a header value by name (case-insensitive).\"\"\"\n        name = name.upper()\n        for (h, v) in self.headers:\n            if h == name:\n                return v\n        return None\n\n    async def read_body(self, size=8192):\n        \"\"\"Read a chunk of the request body.\n\n        Args:\n            size: Maximum bytes to read\n\n        Returns:\n            bytes: Body data, empty bytes when body is exhausted\n        \"\"\"\n        if self._body_remaining == 0:\n            return b\"\"\n\n        if self.chunked:\n            return await self._read_chunked_body(size)\n        else:\n            return await self._read_length_body(size)\n\n    async def _read_length_body(self, size):\n        \"\"\"Read from a length-delimited body.\"\"\"\n        if self._body_remaining <= 0:\n            return b\"\"\n\n        to_read = min(size, self._body_remaining)\n        data = await self.unreader.read(to_read)\n        if data:\n            self._body_remaining -= len(data)\n        return data\n\n    async def _read_chunked_body(self, size):\n        \"\"\"Read from a chunked body.\"\"\"\n        if self._body_reader is None:\n            self._body_reader = self._chunked_body_reader()\n\n        try:\n            return await anext(self._body_reader)\n        except StopAsyncIteration:\n            self._body_remaining = 0\n            return b\"\"\n\n    async def _chunked_body_reader(self):\n        \"\"\"Async generator for reading chunked body.\"\"\"\n        while True:\n            # Read chunk size line\n            size_line = await self._read_chunk_size_line()\n            # Parse chunk size (handle extensions)\n            chunk_size, *_ = size_line.split(b\";\", 1)\n            if _:\n                chunk_size = chunk_size.rstrip(b\" \\t\")\n\n            if any(n not in b\"0123456789abcdefABCDEF\" for n in chunk_size):\n                raise InvalidHeader(\"Invalid chunk size\")\n            if len(chunk_size) == 0:\n                raise InvalidHeader(\"Invalid chunk size\")\n\n            chunk_size = int(chunk_size, 16)\n\n            if chunk_size == 0:\n                # Final chunk - skip trailers and final CRLF\n                await self._skip_trailers()\n                return\n\n            # Read chunk data\n            remaining = chunk_size\n            while remaining > 0:\n                data = await self.unreader.read(min(remaining, 8192))\n                if not data:\n                    raise NoMoreData()\n                remaining -= len(data)\n                yield data\n\n            # Skip chunk terminating CRLF\n            crlf = await self.unreader.read(2)\n            if crlf != b\"\\r\\n\":\n                # May have partial read, try to get the rest\n                while len(crlf) < 2:\n                    more = await self.unreader.read(2 - len(crlf))\n                    if not more:\n                        break\n                    crlf += more\n                if crlf != b\"\\r\\n\":\n                    raise InvalidHeader(\"Missing chunk terminator\")\n\n    async def _read_chunk_size_line(self):\n        \"\"\"Read a chunk size line.\n\n        Performance optimization: reads 64-byte chunks instead of 1 byte at a time,\n        then pushes back any excess data after finding the line terminator.\n        \"\"\"\n        buf = bytearray()\n        while True:\n            data = await self.unreader.read(64)\n            if not data:\n                raise NoMoreData()\n            buf.extend(data)\n            idx = buf.find(b\"\\r\\n\")\n            if idx >= 0:\n                # Push back any data after the line\n                if idx + 2 < len(buf):\n                    self.unreader.unread(bytes(buf[idx + 2:]))\n                return bytes(buf[:idx])\n\n    async def _skip_trailers(self):\n        \"\"\"Skip trailer headers after chunked body.\n\n        Performance optimization: reads 64-byte chunks instead of 1 byte at a time,\n        then pushes back any excess data after finding the trailer terminator.\n        \"\"\"\n        buf = bytearray()\n        while True:\n            data = await self.unreader.read(64)\n            if not data:\n                return\n            buf.extend(data)\n            # Check for empty trailer (just CRLF)\n            if buf[:2] == b\"\\r\\n\":\n                # Push back remaining data\n                if len(buf) > 2:\n                    self.unreader.unread(bytes(buf[2:]))\n                return\n            # Check for full trailer terminator\n            idx = buf.find(b\"\\r\\n\\r\\n\")\n            if idx >= 0:\n                # Push back data after the trailer\n                if idx + 4 < len(buf):\n                    self.unreader.unread(bytes(buf[idx + 4:]))\n                return\n\n    async def drain_body(self):\n        \"\"\"Drain any unread body data.\n\n        Should be called before reusing connection for keepalive.\n        \"\"\"\n        while True:\n            data = await self.read_body(8192)\n            if not data:\n                break\n"
  },
  {
    "path": "gunicorn/asgi/protocol.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nASGI protocol handler for gunicorn.\n\nImplements asyncio.Protocol to handle HTTP/1.x and HTTP/2 connections\nand dispatch to ASGI applications.\n\"\"\"\n\nimport asyncio\nimport errno\nfrom datetime import datetime\n\nfrom gunicorn.asgi.unreader import AsyncUnreader\nfrom gunicorn.asgi.message import AsyncRequest\nfrom gunicorn.asgi.uwsgi import AsyncUWSGIRequest\nfrom gunicorn.http.errors import NoMoreData\nfrom gunicorn.uwsgi.errors import UWSGIParseException\n\n\ndef _normalize_sockaddr(sockaddr):\n    \"\"\"Normalize socket address to ASGI-compatible (host, port) tuple.\n\n    ASGI spec requires server/client to be (host, port) tuples.\n    IPv6 sockets return 4-tuples (host, port, flowinfo, scope_id),\n    so we extract just the first two elements.\n    \"\"\"\n    return tuple(sockaddr[:2]) if sockaddr else None\n\n\nclass ASGIResponseInfo:\n    \"\"\"Simple container for ASGI response info for access logging.\"\"\"\n\n    def __init__(self, status, headers, sent):\n        self.status = status\n        self.sent = sent\n        # Convert headers to list of string tuples for logging\n        self.headers = []\n        for name, value in headers:\n            if isinstance(name, bytes):\n                name = name.decode(\"latin-1\")\n            if isinstance(value, bytes):\n                value = value.decode(\"latin-1\")\n            self.headers.append((name, value))\n\n\nclass ASGIProtocol(asyncio.Protocol):\n    \"\"\"HTTP/1.1 protocol handler for ASGI applications.\n\n    Handles connection lifecycle, request parsing, and ASGI app invocation.\n    \"\"\"\n\n    def __init__(self, worker):\n        self.worker = worker\n        self.cfg = worker.cfg\n        self.log = worker.log\n        self.app = worker.asgi\n\n        self.transport = None\n        self.reader = None\n        self.writer = None\n        self._task = None\n        self.req_count = 0\n\n        # Connection state\n        self._closed = False\n        self._receive_queue = None  # Set per-request for disconnect signaling\n\n    def connection_made(self, transport):\n        \"\"\"Called when a connection is established.\"\"\"\n        self.transport = transport\n        self.worker.nr_conns += 1\n\n        # Check if HTTP/2 was negotiated via ALPN\n        ssl_object = transport.get_extra_info('ssl_object')\n        if ssl_object and hasattr(ssl_object, 'selected_alpn_protocol'):\n            alpn = ssl_object.selected_alpn_protocol()\n            if alpn == 'h2':\n                # HTTP/2 connection - create reader immediately to avoid race condition\n                # data_received may be called before _handle_http2_connection starts\n                self.reader = asyncio.StreamReader()\n                self._task = self.worker.loop.create_task(\n                    self._handle_http2_connection(transport, ssl_object)\n                )\n                return\n\n        # HTTP/1.x connection\n        # Create stream reader/writer\n        self.reader = asyncio.StreamReader()\n        self.writer = transport\n\n        # Start handling requests\n        self._task = self.worker.loop.create_task(self._handle_connection())\n\n    def data_received(self, data):\n        \"\"\"Called when data is received on the connection.\"\"\"\n        if self.reader:\n            self.reader.feed_data(data)\n\n    def connection_lost(self, exc):\n        \"\"\"Called when the connection is lost or closed.\n\n        Instead of immediately cancelling the task, we signal a disconnect\n        event and send an http.disconnect message to the receive queue.\n        This allows the ASGI app to clean up resources (like database\n        connections) gracefully before the task is cancelled.\n\n        See: https://github.com/benoitc/gunicorn/issues/3484\n        \"\"\"\n        # Guard against multiple calls (idempotent)\n        if self._closed:\n            return\n\n        self._closed = True\n        self.worker.nr_conns -= 1\n        if self.reader:\n            self.reader.feed_eof()\n\n        # Signal disconnect to the app via the receive queue\n        if self._receive_queue is not None:\n            self._receive_queue.put_nowait({\"type\": \"http.disconnect\"})\n\n        # Schedule task cancellation after grace period if task doesn't complete\n        if self._task and not self._task.done():\n            grace_period = getattr(self.cfg, 'asgi_disconnect_grace_period', 3)\n            if grace_period > 0:\n                self.worker.loop.call_later(\n                    grace_period,\n                    self._cancel_task_if_pending\n                )\n            else:\n                # Grace period of 0 means cancel immediately\n                self._task.cancel()\n\n    def _cancel_task_if_pending(self):\n        \"\"\"Cancel the task if it's still pending after grace period.\"\"\"\n        if self._task and not self._task.done():\n            self._task.cancel()\n\n    def _safe_write(self, data):\n        \"\"\"Write data to transport, handling connection errors gracefully.\n\n        Catches exceptions that occur when the client has disconnected:\n        - OSError with errno EPIPE, ECONNRESET, ENOTCONN\n        - RuntimeError when transport is closing/closed\n        - AttributeError when transport is None\n\n        These are silently ignored since the client is already gone.\n        \"\"\"\n        try:\n            self.transport.write(data)\n        except OSError as e:\n            if e.errno not in (errno.EPIPE, errno.ECONNRESET, errno.ENOTCONN):\n                self.log.exception(\"Socket error writing response.\")\n        except (RuntimeError, AttributeError):\n            # Transport is closing/closed or None\n            pass\n\n    async def _handle_connection(self):\n        \"\"\"Main request handling loop for this connection.\"\"\"\n        unreader = AsyncUnreader(self.reader)\n\n        try:\n            peername = self.transport.get_extra_info('peername')\n            sockname = self.transport.get_extra_info('sockname')\n\n            while not self._closed:\n                self.req_count += 1\n\n                try:\n                    # Parse request based on protocol\n                    protocol = getattr(self.cfg, 'protocol', 'http')\n                    if protocol == 'uwsgi':\n                        request = await AsyncUWSGIRequest.parse(\n                            self.cfg,\n                            unreader,\n                            peername,\n                            self.req_count\n                        )\n                    else:\n                        request = await AsyncRequest.parse(\n                            self.cfg,\n                            unreader,\n                            peername,\n                            self.req_count\n                        )\n                except NoMoreData:\n                    # Client disconnected\n                    break\n                except UWSGIParseException as e:\n                    self.log.debug(\"uWSGI parse error: %s\", e)\n                    break\n\n                # Check for WebSocket upgrade\n                if self._is_websocket_upgrade(request):\n                    await self._handle_websocket(request, sockname, peername)\n                    break  # WebSocket takes over the connection\n\n                # Handle HTTP request\n                keepalive = await self._handle_http_request(\n                    request, sockname, peername\n                )\n\n                # Increment worker request count\n                self.worker.nr += 1\n\n                # Check max_requests\n                if self.worker.nr >= self.worker.max_requests:\n                    self.log.info(\"Autorestarting worker after current request.\")\n                    self.worker.alive = False\n                    keepalive = False\n\n                if not keepalive or not self.worker.alive:\n                    break\n\n                # Check connection limits for keepalive\n                if not self.cfg.keepalive:\n                    break\n\n                # Drain any unread body before next request\n                await request.drain_body()\n\n        except asyncio.CancelledError:\n            pass\n        except Exception as e:\n            self.log.exception(\"Error handling connection: %s\", e)\n        finally:\n            self._close_transport()\n\n    def _is_websocket_upgrade(self, request):\n        \"\"\"Check if request is a WebSocket upgrade.\n\n        Per RFC 6455 Section 4.1, the opening handshake requires:\n        - HTTP method MUST be GET\n        - Upgrade header MUST be \"websocket\" (case-insensitive)\n        - Connection header MUST contain \"Upgrade\"\n        \"\"\"\n        # RFC 6455: The method of the request MUST be GET\n        if request.method != \"GET\":\n            return False\n\n        upgrade = None\n        connection = None\n        for name, value in request.headers:\n            if name == \"UPGRADE\":\n                upgrade = value.lower()\n            elif name == \"CONNECTION\":\n                connection = value.lower()\n        return upgrade == \"websocket\" and connection and \"upgrade\" in connection\n\n    async def _handle_websocket(self, request, sockname, peername):\n        \"\"\"Handle WebSocket upgrade request.\"\"\"\n        from gunicorn.asgi.websocket import WebSocketProtocol\n\n        scope = self._build_websocket_scope(request, sockname, peername)\n        ws_protocol = WebSocketProtocol(\n            self.transport, self.reader, scope, self.app, self.log\n        )\n        await ws_protocol.run()\n\n    async def _handle_http_request(self, request, sockname, peername):\n        \"\"\"Handle a single HTTP request.\"\"\"\n        scope = self._build_http_scope(request, sockname, peername)\n        response_started = False\n        response_complete = False\n        exc_to_raise = None\n        use_chunked = False\n\n        # Response tracking for access logging\n        response_status = 500\n        response_headers = []\n        response_sent = 0\n\n        # Receive queue for body - stored on self for disconnect signaling\n        receive_queue = asyncio.Queue()\n        self._receive_queue = receive_queue\n        body_complete = False\n\n        # Pre-populate with initial body state\n        if request.content_length == 0 and not request.chunked:\n            await receive_queue.put({\n                \"type\": \"http.request\",\n                \"body\": b\"\",\n                \"more_body\": False,\n            })\n            body_complete = True\n        else:\n            # Start body reading task\n            asyncio.create_task(self._read_body_to_queue(request, receive_queue))\n\n        async def receive():\n            nonlocal body_complete\n            # Check if already disconnected before waiting\n            if self._closed and body_complete:\n                return {\"type\": \"http.disconnect\"}\n\n            msg = await receive_queue.get()\n\n            # Track when body is complete\n            if msg.get(\"type\") == \"http.request\" and not msg.get(\"more_body\", True):\n                body_complete = True\n\n            return msg\n\n        async def send(message):\n            nonlocal response_started, response_complete, exc_to_raise\n            nonlocal response_status, response_headers, response_sent, use_chunked\n\n            # If client disconnected, silently ignore send attempts\n            # This allows apps to finish cleanup without errors\n            if self._closed:\n                return\n\n            msg_type = message[\"type\"]\n\n            if msg_type == \"http.response.informational\":\n                # Handle informational responses (1xx) like 103 Early Hints\n                info_status = message.get(\"status\")\n                info_headers = message.get(\"headers\", [])\n                await self._send_informational(info_status, info_headers, request)\n                return\n\n            if msg_type == \"http.response.start\":\n                if response_started:\n                    exc_to_raise = RuntimeError(\"Response already started\")\n                    return\n                response_started = True\n                response_status = message[\"status\"]\n                response_headers = message.get(\"headers\", [])\n\n                # Check if Content-Length is present\n                has_content_length = any(\n                    (name.lower() if isinstance(name, str) else name.lower()) == b\"content-length\"\n                    or (name.lower() if isinstance(name, str) else name.lower()) == \"content-length\"\n                    for name, _ in response_headers\n                )\n\n                # Use chunked encoding for HTTP/1.1 streaming responses without Content-Length\n                if not has_content_length and request.version >= (1, 1):\n                    use_chunked = True\n                    response_headers = list(response_headers) + [(b\"transfer-encoding\", b\"chunked\")]\n\n                await self._send_response_start(response_status, response_headers, request)\n\n            elif msg_type == \"http.response.body\":\n                if not response_started:\n                    exc_to_raise = RuntimeError(\"Response not started\")\n                    return\n                if response_complete:\n                    exc_to_raise = RuntimeError(\"Response already complete\")\n                    return\n\n                body = message.get(\"body\", b\"\")\n                more_body = message.get(\"more_body\", False)\n\n                if body:\n                    await self._send_body(body, chunked=use_chunked)\n                    response_sent += len(body)\n\n                if not more_body:\n                    if use_chunked:\n                        # Send terminal chunk\n                        self._safe_write(b\"0\\r\\n\\r\\n\")\n                    response_complete = True\n\n        # Build environ for logging\n        environ = self._build_environ(request, sockname, peername)\n        resp = None\n\n        try:\n            request_start = datetime.now()\n            self.cfg.pre_request(self.worker, request)\n\n            await self.app(scope, receive, send)\n\n            if exc_to_raise is not None:\n                raise exc_to_raise\n\n            # Ensure response was sent\n            if not response_started:\n                await self._send_error_response(500, \"Internal Server Error\")\n                response_status = 500\n\n        except asyncio.CancelledError:\n            # Client disconnected - don't log as error, this is normal\n            self.log.debug(\"Request cancelled (client disconnected)\")\n            return False\n        except Exception:\n            self.log.exception(\"Error in ASGI application\")\n            if not response_started:\n                await self._send_error_response(500, \"Internal Server Error\")\n                response_status = 500\n            return False\n        finally:\n            # Clear the receive queue reference\n            self._receive_queue = None\n\n            try:\n                request_time = datetime.now() - request_start\n                # Create response info for logging\n                resp = ASGIResponseInfo(response_status, response_headers, response_sent)\n                self.log.access(resp, request, environ, request_time)\n                self.cfg.post_request(self.worker, request, environ, resp)\n            except Exception:\n                self.log.exception(\"Exception in post_request hook\")\n\n        # Determine keepalive\n        if request.should_close():\n            return False\n\n        return self.worker.alive and self.cfg.keepalive\n\n    async def _read_body_to_queue(self, request, queue):\n        \"\"\"Read request body and put chunks on the queue.\"\"\"\n        try:\n            while True:\n                chunk = await request.read_body(65536)\n                if chunk:\n                    await queue.put({\n                        \"type\": \"http.request\",\n                        \"body\": chunk,\n                        \"more_body\": True,\n                    })\n                else:\n                    await queue.put({\n                        \"type\": \"http.request\",\n                        \"body\": b\"\",\n                        \"more_body\": False,\n                    })\n                    break\n        except Exception as e:\n            self.log.debug(\"Error reading body: %s\", e)\n            await queue.put({\n                \"type\": \"http.request\",\n                \"body\": b\"\",\n                \"more_body\": False,\n            })\n\n    def _build_http_scope(self, request, sockname, peername):\n        \"\"\"Build ASGI HTTP scope from parsed request.\"\"\"\n        # Build headers list as bytes tuples\n        headers = []\n        for name, value in request.headers:\n            headers.append((name.lower().encode(\"latin-1\"), value.encode(\"latin-1\")))\n\n        server = _normalize_sockaddr(sockname)\n        client = _normalize_sockaddr(peername)\n\n        scope = {\n            \"type\": \"http\",\n            \"asgi\": {\"version\": \"3.0\", \"spec_version\": \"2.4\"},\n            \"http_version\": f\"{request.version[0]}.{request.version[1]}\",\n            \"method\": request.method,\n            \"scheme\": request.scheme,\n            \"path\": request.path,\n            \"raw_path\": request.path.encode(\"latin-1\") if request.path else b\"\",\n            \"query_string\": request.query.encode(\"latin-1\") if request.query else b\"\",\n            \"root_path\": self.cfg.root_path or \"\",\n            \"headers\": headers,\n            \"server\": server,\n            \"client\": client,\n        }\n\n        # Add state dict for lifespan sharing\n        if hasattr(self.worker, 'state'):\n            scope[\"state\"] = self.worker.state\n\n        # Add HTTP/2 priority extension if available\n        if hasattr(request, 'priority_weight'):\n            scope[\"extensions\"] = {\n                \"http.response.priority\": {\n                    \"weight\": request.priority_weight,\n                    \"depends_on\": request.priority_depends_on,\n                }\n            }\n\n        return scope\n\n    def _build_environ(self, request, sockname, peername):\n        \"\"\"Build minimal WSGI-like environ dict for access logging.\"\"\"\n        environ = {\n            \"REQUEST_METHOD\": request.method,\n            \"RAW_URI\": request.uri,\n            \"PATH_INFO\": request.path,\n            \"QUERY_STRING\": request.query or \"\",\n            \"SERVER_PROTOCOL\": f\"HTTP/{request.version[0]}.{request.version[1]}\",\n            \"REMOTE_ADDR\": peername[0] if peername else \"-\",\n        }\n\n        # Add HTTP headers as environ vars\n        for name, value in request.headers:\n            key = \"HTTP_\" + name.replace(\"-\", \"_\")\n            environ[key] = value\n\n        return environ\n\n    def _build_websocket_scope(self, request, sockname, peername):\n        \"\"\"Build ASGI WebSocket scope from parsed request.\"\"\"\n        # Build headers list as bytes tuples\n        headers = []\n        for name, value in request.headers:\n            headers.append((name.lower().encode(\"latin-1\"), value.encode(\"latin-1\")))\n\n        # Extract subprotocols from Sec-WebSocket-Protocol header\n        subprotocols = []\n        for name, value in request.headers:\n            if name == \"SEC-WEBSOCKET-PROTOCOL\":\n                subprotocols = [s.strip() for s in value.split(\",\")]\n                break\n\n        server = _normalize_sockaddr(sockname)\n        client = _normalize_sockaddr(peername)\n\n        scope = {\n            \"type\": \"websocket\",\n            \"asgi\": {\"version\": \"3.0\", \"spec_version\": \"2.4\"},\n            \"http_version\": f\"{request.version[0]}.{request.version[1]}\",\n            \"scheme\": \"wss\" if request.scheme == \"https\" else \"ws\",\n            \"path\": request.path,\n            \"raw_path\": request.path.encode(\"latin-1\") if request.path else b\"\",\n            \"query_string\": request.query.encode(\"latin-1\") if request.query else b\"\",\n            \"root_path\": self.cfg.root_path or \"\",\n            \"headers\": headers,\n            \"server\": server,\n            \"client\": client,\n            \"subprotocols\": subprotocols,\n        }\n\n        # Add state dict for lifespan sharing\n        if hasattr(self.worker, 'state'):\n            scope[\"state\"] = self.worker.state\n\n        return scope\n\n    async def _send_informational(self, status, headers, request):\n        \"\"\"Send an informational response (1xx) such as 103 Early Hints.\n\n        Args:\n            status: HTTP status code (100-199)\n            headers: List of (name, value) header tuples\n            request: The parsed request object\n\n        Note: Informational responses are only sent for HTTP/1.1 or later.\n        HTTP/1.0 clients do not support 1xx responses.\n        \"\"\"\n        # Don't send informational responses to HTTP/1.0 clients\n        if request.version < (1, 1):\n            return\n\n        reason = self._get_reason_phrase(status)\n        response = f\"HTTP/{request.version[0]}.{request.version[1]} {status} {reason}\\r\\n\"\n\n        for name, value in headers:\n            if isinstance(name, bytes):\n                name = name.decode(\"latin-1\")\n            if isinstance(value, bytes):\n                value = value.decode(\"latin-1\")\n            response += f\"{name}: {value}\\r\\n\"\n\n        response += \"\\r\\n\"\n        self._safe_write(response.encode(\"latin-1\"))\n\n    async def _send_response_start(self, status, headers, request):\n        \"\"\"Send HTTP response status and headers.\"\"\"\n        # Build status line\n        reason = self._get_reason_phrase(status)\n        status_line = f\"HTTP/{request.version[0]}.{request.version[1]} {status} {reason}\\r\\n\"\n\n        # Build headers\n        header_lines = []\n\n        for name, value in headers:\n            if isinstance(name, bytes):\n                name = name.decode(\"latin-1\")\n            if isinstance(value, bytes):\n                value = value.decode(\"latin-1\")\n            header_lines.append(f\"{name}: {value}\\r\\n\")\n\n        # Add server header if not present\n        header_lines.append(\"Server: gunicorn/asgi\\r\\n\")\n\n        response = status_line + \"\".join(header_lines) + \"\\r\\n\"\n        self._safe_write(response.encode(\"latin-1\"))\n\n    async def _send_body(self, body, chunked=False):\n        \"\"\"Send response body chunk.\"\"\"\n        if body:\n            if chunked:\n                # Chunked encoding: size in hex + CRLF + data + CRLF\n                chunk = f\"{len(body):x}\\r\\n\".encode(\"latin-1\") + body + b\"\\r\\n\"\n                self._safe_write(chunk)\n            else:\n                self._safe_write(body)\n\n    async def _send_error_response(self, status, message):\n        \"\"\"Send an error response.\"\"\"\n        body = message.encode(\"utf-8\")\n        response = (\n            f\"HTTP/1.1 {status} {message}\\r\\n\"\n            f\"Content-Type: text/plain\\r\\n\"\n            f\"Content-Length: {len(body)}\\r\\n\"\n            f\"Connection: close\\r\\n\"\n            f\"\\r\\n\"\n        )\n        self._safe_write(response.encode(\"latin-1\"))\n        self._safe_write(body)\n\n    def _get_reason_phrase(self, status):\n        \"\"\"Get HTTP reason phrase for status code.\"\"\"\n        reasons = {\n            100: \"Continue\",\n            101: \"Switching Protocols\",\n            103: \"Early Hints\",\n            200: \"OK\",\n            201: \"Created\",\n            202: \"Accepted\",\n            204: \"No Content\",\n            206: \"Partial Content\",\n            301: \"Moved Permanently\",\n            302: \"Found\",\n            303: \"See Other\",\n            304: \"Not Modified\",\n            307: \"Temporary Redirect\",\n            308: \"Permanent Redirect\",\n            400: \"Bad Request\",\n            401: \"Unauthorized\",\n            403: \"Forbidden\",\n            404: \"Not Found\",\n            405: \"Method Not Allowed\",\n            408: \"Request Timeout\",\n            409: \"Conflict\",\n            410: \"Gone\",\n            411: \"Length Required\",\n            413: \"Payload Too Large\",\n            414: \"URI Too Long\",\n            415: \"Unsupported Media Type\",\n            422: \"Unprocessable Entity\",\n            429: \"Too Many Requests\",\n            500: \"Internal Server Error\",\n            501: \"Not Implemented\",\n            502: \"Bad Gateway\",\n            503: \"Service Unavailable\",\n            504: \"Gateway Timeout\",\n        }\n        return reasons.get(status, \"Unknown\")\n\n    def _close_transport(self):\n        \"\"\"Close the transport safely.\n\n        Calls write_eof() first if supported to signal end of writing,\n        which helps ensure buffered data is flushed before closing.\n        \"\"\"\n        if self.transport and not self._closed:\n            try:\n                # Signal end of writing to help flush buffers\n                if self.transport.can_write_eof():\n                    self.transport.write_eof()\n                self.transport.close()\n            except Exception:\n                pass\n            self._closed = True\n\n    async def _handle_http2_connection(self, transport, ssl_object):\n        \"\"\"Handle an HTTP/2 connection.\"\"\"\n        try:\n            from gunicorn.http2.async_connection import AsyncHTTP2Connection\n\n            peername = transport.get_extra_info('peername')\n            sockname = transport.get_extra_info('sockname')\n\n            # Use the reader created in connection_made\n            # (data_received feeds data to self.reader)\n            reader = self.reader\n            protocol = asyncio.StreamReaderProtocol(reader)\n            writer = asyncio.StreamWriter(\n                transport, protocol, reader, self.worker.loop\n            )\n\n            # Create HTTP/2 connection handler\n            h2_conn = AsyncHTTP2Connection(\n                self.cfg, reader, writer, peername\n            )\n            await h2_conn.initiate_connection()\n\n            self._h2_conn = h2_conn\n\n            # Main loop - receive and handle requests\n            while not h2_conn.is_closed and self.worker.alive:\n                try:\n                    requests = await h2_conn.receive_data(timeout=1.0)\n                except asyncio.TimeoutError:\n                    continue\n                except Exception as e:\n                    self.log.debug(\"HTTP/2 receive error: %s\", e)\n                    break\n\n                for req in requests:\n                    try:\n                        await self._handle_http2_request(\n                            req, h2_conn, sockname, peername\n                        )\n                    except Exception as e:\n                        self.log.exception(\"Error handling HTTP/2 request\")\n                        try:\n                            await h2_conn.send_error(\n                                req.stream.stream_id, 500, str(e)\n                            )\n                        except Exception:\n                            pass\n                    finally:\n                        h2_conn.cleanup_stream(req.stream.stream_id)\n\n                # Increment worker request count\n                self.worker.nr += len(requests)\n\n                # Check max_requests\n                if self.worker.nr >= self.worker.max_requests:\n                    self.log.info(\"Autorestarting worker after current request.\")\n                    self.worker.alive = False\n                    break\n\n        except asyncio.CancelledError:\n            pass\n        except Exception as e:\n            self.log.exception(\"HTTP/2 connection error: %s\", e)\n        finally:\n            if hasattr(self, '_h2_conn'):\n                try:\n                    await self._h2_conn.close()\n                except Exception:\n                    pass\n            self._close_transport()\n\n    async def _handle_http2_request(self, request, h2_conn, sockname, peername):\n        \"\"\"Handle a single HTTP/2 request.\"\"\"\n        stream_id = request.stream.stream_id\n        scope = self._build_http2_scope(request, sockname, peername)\n\n        response_started = False\n        response_complete = False\n        exc_to_raise = None\n\n        response_status = 500\n        response_headers = []\n        response_body = b''\n        response_trailers = []\n\n        async def receive():\n            # For HTTP/2, the body is already buffered in the stream\n            body = request.body.read()\n            return {\n                \"type\": \"http.request\",\n                \"body\": body,\n                \"more_body\": False,\n            }\n\n        async def send(message):\n            nonlocal response_started, response_complete, exc_to_raise\n            nonlocal response_status, response_headers, response_body\n\n            msg_type = message[\"type\"]\n\n            if msg_type == \"http.response.informational\":\n                # Handle informational responses (1xx) like 103 Early Hints over HTTP/2\n                info_status = message.get(\"status\")\n                info_headers = message.get(\"headers\", [])\n                # Convert headers to list of string tuples\n                headers = []\n                for name, value in info_headers:\n                    if isinstance(name, bytes):\n                        name = name.decode(\"latin-1\")\n                    if isinstance(value, bytes):\n                        value = value.decode(\"latin-1\")\n                    headers.append((name, value))\n                await h2_conn.send_informational(stream_id, info_status, headers)\n                return\n\n            if msg_type == \"http.response.start\":\n                if response_started:\n                    exc_to_raise = RuntimeError(\"Response already started\")\n                    return\n                response_started = True\n                response_status = message[\"status\"]\n                response_headers = message.get(\"headers\", [])\n\n            elif msg_type == \"http.response.body\":\n                if not response_started:\n                    exc_to_raise = RuntimeError(\"Response not started\")\n                    return\n                if response_complete:\n                    exc_to_raise = RuntimeError(\"Response already complete\")\n                    return\n\n                body = message.get(\"body\", b\"\")\n                more_body = message.get(\"more_body\", False)\n\n                if body:\n                    response_body += body\n\n                if not more_body:\n                    response_complete = True\n\n            elif msg_type == \"http.response.trailers\":\n                if not response_complete:\n                    exc_to_raise = RuntimeError(\"Cannot send trailers before body complete\")\n                    return\n                trailer_headers = message.get(\"headers\", [])\n                # Convert to list of tuples with string values\n                trailers = []\n                for name, value in trailer_headers:\n                    if isinstance(name, bytes):\n                        name = name.decode(\"latin-1\")\n                    if isinstance(value, bytes):\n                        value = value.decode(\"latin-1\")\n                    trailers.append((name, value))\n                response_trailers.extend(trailers)\n\n        # Build environ for logging\n        environ = self._build_http2_environ(request, sockname, peername)\n        request_start = datetime.now()\n\n        try:\n            self.cfg.pre_request(self.worker, request)\n            await self.app(scope, receive, send)\n\n            if exc_to_raise is not None:\n                raise exc_to_raise\n\n            # Send response via HTTP/2\n            if response_started:\n                # Convert headers to list of tuples\n                headers = []\n                for name, value in response_headers:\n                    if isinstance(name, bytes):\n                        name = name.decode(\"latin-1\")\n                    if isinstance(value, bytes):\n                        value = value.decode(\"latin-1\")\n                    headers.append((name, value))\n\n                if response_trailers:\n                    # Send headers, body, then trailers separately\n                    response_hdrs = [(':status', str(response_status))]\n                    for name, value in headers:\n                        response_hdrs.append((name.lower(), str(value)))\n\n                    # Send headers without ending stream\n                    h2_conn.h2_conn.send_headers(stream_id, response_hdrs, end_stream=False)\n                    stream = h2_conn.streams[stream_id]\n                    stream.send_headers(response_hdrs, end_stream=False)\n                    await h2_conn._send_pending_data()\n\n                    # Send body without ending stream\n                    if response_body:\n                        h2_conn.h2_conn.send_data(stream_id, response_body, end_stream=False)\n                        stream.send_data(response_body, end_stream=False)\n                        await h2_conn._send_pending_data()\n\n                    # Send trailers (ends stream)\n                    await h2_conn.send_trailers(stream_id, response_trailers)\n                else:\n                    await h2_conn.send_response(\n                        stream_id, response_status, headers, response_body\n                    )\n            else:\n                await h2_conn.send_error(stream_id, 500, \"Internal Server Error\")\n                response_status = 500\n\n        except Exception:\n            self.log.exception(\"Error in ASGI application\")\n            if not response_started:\n                await h2_conn.send_error(stream_id, 500, \"Internal Server Error\")\n                response_status = 500\n        finally:\n            try:\n                request_time = datetime.now() - request_start\n                resp = ASGIResponseInfo(\n                    response_status, response_headers, len(response_body)\n                )\n                self.log.access(resp, request, environ, request_time)\n                self.cfg.post_request(self.worker, request, environ, resp)\n            except Exception:\n                self.log.exception(\"Exception in post_request hook\")\n\n    def _build_http2_scope(self, request, sockname, peername):\n        \"\"\"Build ASGI HTTP scope from HTTP/2 request.\"\"\"\n        headers = []\n        for name, value in request.headers:\n            headers.append((\n                name.lower().encode(\"latin-1\"),\n                value.encode(\"latin-1\")\n            ))\n\n        server = _normalize_sockaddr(sockname)\n        client = _normalize_sockaddr(peername)\n\n        scope = {\n            \"type\": \"http\",\n            \"asgi\": {\"version\": \"3.0\", \"spec_version\": \"2.4\"},\n            \"http_version\": \"2\",\n            \"method\": request.method,\n            \"scheme\": request.scheme,\n            \"path\": request.path,\n            \"raw_path\": request.path.encode(\"latin-1\") if request.path else b\"\",\n            \"query_string\": request.query.encode(\"latin-1\") if request.query else b\"\",\n            \"root_path\": self.cfg.root_path or \"\",\n            \"headers\": headers,\n            \"server\": server,\n            \"client\": client,\n        }\n\n        if hasattr(self.worker, 'state'):\n            scope[\"state\"] = self.worker.state\n\n        # Add HTTP/2 extensions\n        extensions = {}\n        if hasattr(request, 'priority_weight'):\n            extensions[\"http.response.priority\"] = {\n                \"weight\": request.priority_weight,\n                \"depends_on\": request.priority_depends_on,\n            }\n        # Add trailer support extension for HTTP/2\n        extensions[\"http.response.trailers\"] = {}\n        scope[\"extensions\"] = extensions\n\n        return scope\n\n    def _build_http2_environ(self, request, sockname, peername):\n        \"\"\"Build minimal environ dict for access logging.\"\"\"\n        environ = {\n            \"REQUEST_METHOD\": request.method,\n            \"RAW_URI\": request.uri,\n            \"PATH_INFO\": request.path,\n            \"QUERY_STRING\": request.query or \"\",\n            \"SERVER_PROTOCOL\": \"HTTP/2\",\n            \"REMOTE_ADDR\": peername[0] if peername else \"-\",\n        }\n\n        for name, value in request.headers:\n            key = \"HTTP_\" + name.replace(\"-\", \"_\")\n            environ[key] = value\n\n        return environ\n"
  },
  {
    "path": "gunicorn/asgi/unreader.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nAsync version of gunicorn/http/unreader.py for ASGI workers.\n\nProvides async reading with pushback buffer support.\n\"\"\"\n\nimport io\n\n\nclass AsyncUnreader:\n    \"\"\"Async socket reader with pushback buffer support.\n\n    This class wraps an asyncio StreamReader and provides the ability\n    to \"unread\" data back into a buffer for re-parsing.\n\n    Performance optimization: Reuses BytesIO buffer with truncate/seek\n    instead of creating new objects to reduce GC pressure.\n    \"\"\"\n\n    def __init__(self, reader, max_chunk=8192):\n        \"\"\"Initialize the async unreader.\n\n        Args:\n            reader: asyncio.StreamReader instance\n            max_chunk: Maximum bytes to read at once\n        \"\"\"\n        self.reader = reader\n        self.buf = io.BytesIO()\n        self.max_chunk = max_chunk\n        self._buf_start = 0  # Start position of valid data in buffer\n\n    def _reset_buffer(self):\n        \"\"\"Reset buffer for reuse instead of creating new BytesIO.\"\"\"\n        self.buf.seek(0)\n        self.buf.truncate(0)\n        self._buf_start = 0\n\n    def _get_buffered_data(self):\n        \"\"\"Get all buffered data and reset buffer.\"\"\"\n        self.buf.seek(self._buf_start)\n        data = self.buf.read()\n        self._reset_buffer()\n        return data\n\n    def _buffer_size(self):\n        \"\"\"Get size of buffered data.\"\"\"\n        end = self.buf.seek(0, io.SEEK_END)\n        return end - self._buf_start\n\n    async def read(self, size=None):\n        \"\"\"Read data from the stream, using buffered data first.\n\n        Args:\n            size: Number of bytes to read. If None, returns all buffered\n                  data or reads a single chunk.\n\n        Returns:\n            bytes: Data read from buffer or stream\n        \"\"\"\n        if size is not None and not isinstance(size, int):\n            raise TypeError(\"size parameter must be an int or long.\")\n\n        if size is not None:\n            if size == 0:\n                return b\"\"\n            if size < 0:\n                size = None\n\n        buf_size = self._buffer_size()\n\n        # If no size specified, return buffered data or read chunk\n        if size is None and buf_size > 0:\n            return self._get_buffered_data()\n        if size is None:\n            chunk = await self._read_chunk()\n            return chunk\n\n        # Read until we have enough data\n        while buf_size < size:\n            chunk = await self._read_chunk()\n            if not chunk:\n                return self._get_buffered_data()\n            self.buf.seek(0, io.SEEK_END)\n            self.buf.write(chunk)\n            buf_size += len(chunk)\n\n        # We have enough data - extract what we need\n        self.buf.seek(self._buf_start)\n        data = self.buf.read(size)\n\n        # Update start position instead of creating new buffer\n        self._buf_start += size\n\n        # If buffer is getting large with consumed data, compact it\n        if self._buf_start > 8192:\n            remaining = self.buf.read()  # Read from current position\n            self._reset_buffer()\n            if remaining:\n                self.buf.write(remaining)\n\n        return data\n\n    async def _read_chunk(self):\n        \"\"\"Read a chunk of data from the underlying stream.\"\"\"\n        try:\n            return await self.reader.read(self.max_chunk)\n        except Exception:\n            return b\"\"\n\n    def unread(self, data):\n        \"\"\"Push data back into the buffer for re-reading.\n\n        Args:\n            data: bytes to push back\n\n        Note: This prepends data to the buffer so it will be read first.\n        \"\"\"\n        if data:\n            # Get existing buffered data\n            self.buf.seek(self._buf_start)\n            existing = self.buf.read()\n\n            # Reset and write new data first, then existing\n            self._reset_buffer()\n            self.buf.write(data)\n            if existing:\n                self.buf.write(existing)\n\n    def has_buffered_data(self):\n        \"\"\"Check if there's data in the pushback buffer.\"\"\"\n        return self._buffer_size() > 0\n"
  },
  {
    "path": "gunicorn/asgi/uwsgi.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Async uWSGI protocol parser for ASGI workers.\n\nReuses the parsing logic from gunicorn/uwsgi/message.py, only async I/O differs.\n\"\"\"\n\nfrom gunicorn.uwsgi.message import UWSGIRequest\nfrom gunicorn.uwsgi.errors import (\n    InvalidUWSGIHeader,\n    UnsupportedModifier,\n)\n\n\nclass AsyncUWSGIRequest(UWSGIRequest):\n    \"\"\"Async version of UWSGIRequest.\n\n    Reuses all parsing logic from the sync version, only async I/O differs.\n    The following methods are reused from the parent class:\n    - _parse_vars() - pure parsing, no I/O\n    - _extract_request_info() - pure transformation\n    - _check_allowed_ip() - no I/O\n    - should_close() - simple logic\n    \"\"\"\n\n    # pylint: disable=super-init-not-called\n    def __init__(self, cfg, unreader, peer_addr, req_number=1):\n        # Don't call super().__init__ - it does sync parsing\n        # Just initialize attributes\n        self.cfg = cfg\n        self.unreader = unreader\n        self.peer_addr = peer_addr\n        self.remote_addr = peer_addr\n        self.req_number = req_number\n\n        # Initialize all attributes (same as sync version)\n        self.method = None\n        self.uri = None\n        self.path = None\n        self.query = None\n        self.fragment = \"\"\n        self.version = (1, 1)\n        self.headers = []\n        self.trailers = []\n        self.body = None\n        self.scheme = \"https\" if cfg.is_ssl else \"http\"\n        self.must_close = False\n        self.uwsgi_vars = {}\n        self.modifier1 = 0\n        self.modifier2 = 0\n        self.proxy_protocol_info = None\n\n        # Body state\n        self.content_length = 0\n        self.chunked = False\n        self._body_remaining = 0\n\n    # Async factory method - intentionally differs from sync parent:\n    # - async instead of sync (invalid-overridden-method)\n    # - different signature for async I/O (arguments-differ)\n    # pylint: disable=arguments-differ,invalid-overridden-method\n    @classmethod\n    async def parse(cls, cfg, unreader, peer_addr, req_number=1):\n        \"\"\"Parse a uWSGI request asynchronously.\n\n        Args:\n            cfg: gunicorn config object\n            unreader: AsyncUnreader instance\n            peer_addr: client address tuple\n            req_number: request number on this connection (for keepalive)\n\n        Returns:\n            AsyncUWSGIRequest: Parsed request object\n\n        Raises:\n            InvalidUWSGIHeader: If the uWSGI header is malformed\n            UnsupportedModifier: If modifier1 is not 0\n            ForbiddenUWSGIRequest: If source IP is not allowed\n        \"\"\"\n        req = cls(cfg, unreader, peer_addr, req_number)\n        req._check_allowed_ip()  # Reuse from parent\n        await req._async_parse()\n        return req\n\n    async def _async_parse(self):\n        \"\"\"Async version of parse() - reads data then uses sync parsing.\"\"\"\n        # Read 4-byte header\n        header = await self._async_read_exact(4)\n        if len(header) < 4:\n            raise InvalidUWSGIHeader(\"incomplete header\")\n\n        self.modifier1 = header[0]\n        datasize = int.from_bytes(header[1:3], 'little')\n        self.modifier2 = header[3]\n\n        if self.modifier1 != 0:\n            raise UnsupportedModifier(self.modifier1)\n\n        # Read vars block\n        if datasize > 0:\n            vars_data = await self._async_read_exact(datasize)\n            if len(vars_data) < datasize:\n                raise InvalidUWSGIHeader(\"incomplete vars block\")\n            self._parse_vars(vars_data)  # Reuse sync method\n\n        self._extract_request_info()  # Reuse sync method\n        self._set_body_reader()\n\n    async def _async_read_exact(self, size):\n        \"\"\"Read exactly size bytes asynchronously.\"\"\"\n        buf = bytearray()\n        while len(buf) < size:\n            chunk = await self.unreader.read(size - len(buf))\n            if not chunk:\n                break\n            buf.extend(chunk)\n        return bytes(buf)\n\n    def _set_body_reader(self):\n        \"\"\"Set up body state for async reading.\"\"\"\n        content_length = 0\n        if 'CONTENT_LENGTH' in self.uwsgi_vars:\n            try:\n                content_length = max(int(self.uwsgi_vars['CONTENT_LENGTH']), 0)\n            except ValueError:\n                content_length = 0\n        self.content_length = content_length\n        self._body_remaining = content_length\n\n    async def read_body(self, size=8192):\n        \"\"\"Read body chunk asynchronously.\n\n        Args:\n            size: Maximum bytes to read\n\n        Returns:\n            bytes: Body data, empty bytes when body is exhausted\n        \"\"\"\n        if self._body_remaining <= 0:\n            return b\"\"\n        to_read = min(size, self._body_remaining)\n        data = await self.unreader.read(to_read)\n        if data:\n            self._body_remaining -= len(data)\n        return data\n\n    async def drain_body(self):\n        \"\"\"Drain unread body data.\n\n        Should be called before reusing connection for keepalive.\n        \"\"\"\n        while self._body_remaining > 0:\n            data = await self.read_body(8192)\n            if not data:\n                break\n\n    def get_header(self, name):\n        \"\"\"Get header by name (case-insensitive).\n\n        Args:\n            name: Header name to look up\n\n        Returns:\n            Header value if found, None otherwise\n        \"\"\"\n        name = name.upper()\n        for h, v in self.headers:\n            if h == name:\n                return v\n        return None\n"
  },
  {
    "path": "gunicorn/asgi/websocket.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nWebSocket protocol handler for ASGI.\n\nImplements RFC 6455 WebSocket protocol for ASGI applications.\n\"\"\"\n\nimport asyncio\nimport base64\nimport hashlib\nimport struct\n\n\n# WebSocket frame opcodes\nOPCODE_CONTINUATION = 0x0\nOPCODE_TEXT = 0x1\nOPCODE_BINARY = 0x2\nOPCODE_CLOSE = 0x8\nOPCODE_PING = 0x9\nOPCODE_PONG = 0xA\n\n# WebSocket close codes\nCLOSE_NORMAL = 1000\nCLOSE_GOING_AWAY = 1001\nCLOSE_PROTOCOL_ERROR = 1002\nCLOSE_UNSUPPORTED = 1003\nCLOSE_NO_STATUS = 1005\nCLOSE_ABNORMAL = 1006\nCLOSE_INVALID_DATA = 1007\nCLOSE_POLICY_VIOLATION = 1008\nCLOSE_MESSAGE_TOO_BIG = 1009\nCLOSE_MANDATORY_EXT = 1010\nCLOSE_INTERNAL_ERROR = 1011\n\n# WebSocket handshake GUID (RFC 6455)\nWS_GUID = b\"258EAFA5-E914-47DA-95CA-C5AB0DC85B11\"\n\n\nclass WebSocketProtocol:\n    \"\"\"WebSocket connection handler for ASGI applications.\"\"\"\n\n    def __init__(self, transport, reader, scope, app, log):\n        \"\"\"Initialize WebSocket protocol handler.\n\n        Args:\n            transport: asyncio transport for writing\n            reader: asyncio StreamReader for reading\n            scope: ASGI WebSocket scope dict\n            app: ASGI application callable\n            log: Logger instance\n        \"\"\"\n        self.transport = transport\n        self.reader = reader\n        self.scope = scope\n        self.app = app\n        self.log = log\n\n        self.accepted = False\n        self.closed = False\n        self.close_code = None\n        self.close_reason = \"\"\n\n        # Message reassembly state\n        self._fragments = []\n        self._fragment_opcode = None\n\n        # Receive queue for incoming messages\n        self._receive_queue = asyncio.Queue()\n\n    async def run(self):\n        \"\"\"Run the WebSocket ASGI application.\"\"\"\n        # Send initial connect event\n        await self._receive_queue.put({\"type\": \"websocket.connect\"})\n\n        # Start frame reading task\n        read_task = asyncio.create_task(self._read_frames())\n\n        try:\n            await self.app(self.scope, self._receive, self._send)\n        except Exception:\n            self.log.exception(\"Error in WebSocket ASGI application\")\n        finally:\n            read_task.cancel()\n            try:\n                await read_task\n            except asyncio.CancelledError:\n                pass\n\n            # Send close frame if not already closed\n            if not self.closed and self.accepted:\n                await self._send_close(CLOSE_INTERNAL_ERROR, \"Application error\")\n\n    async def _receive(self):\n        \"\"\"ASGI receive callable.\"\"\"\n        return await self._receive_queue.get()\n\n    async def _send(self, message):\n        \"\"\"ASGI send callable.\"\"\"\n        msg_type = message[\"type\"]\n\n        if msg_type == \"websocket.accept\":\n            if self.accepted:\n                raise RuntimeError(\"WebSocket already accepted\")\n            await self._send_accept(message)\n            self.accepted = True\n\n        elif msg_type == \"websocket.send\":\n            if not self.accepted:\n                raise RuntimeError(\"WebSocket not accepted\")\n            if self.closed:\n                raise RuntimeError(\"WebSocket closed\")\n\n            if \"text\" in message:\n                await self._send_frame(OPCODE_TEXT, message[\"text\"].encode(\"utf-8\"))\n            elif \"bytes\" in message:\n                await self._send_frame(OPCODE_BINARY, message[\"bytes\"])\n\n        elif msg_type == \"websocket.close\":\n            code = message.get(\"code\", CLOSE_NORMAL)\n            reason = message.get(\"reason\", \"\")\n            await self._send_close(code, reason)\n            self.closed = True\n\n    async def _send_accept(self, message):\n        \"\"\"Send WebSocket handshake accept response.\"\"\"\n        # Get Sec-WebSocket-Key from headers\n        ws_key = None\n        for name, value in self.scope[\"headers\"]:\n            if name == b\"sec-websocket-key\":\n                ws_key = value\n                break\n\n        if not ws_key:\n            raise RuntimeError(\"Missing Sec-WebSocket-Key header\")\n\n        # Calculate accept key\n        accept_key = base64.b64encode(\n            hashlib.sha1(ws_key + WS_GUID).digest()\n        ).decode(\"ascii\")\n\n        # Build response headers\n        headers = [\n            \"HTTP/1.1 101 Switching Protocols\\r\\n\",\n            \"Upgrade: websocket\\r\\n\",\n            \"Connection: Upgrade\\r\\n\",\n            f\"Sec-WebSocket-Accept: {accept_key}\\r\\n\",\n        ]\n\n        # Add selected subprotocol if specified\n        subprotocol = message.get(\"subprotocol\")\n        if subprotocol:\n            headers.append(f\"Sec-WebSocket-Protocol: {subprotocol}\\r\\n\")\n\n        # Add any extra headers from message\n        extra_headers = message.get(\"headers\", [])\n        for name, value in extra_headers:\n            if isinstance(name, bytes):\n                name = name.decode(\"latin-1\")\n            if isinstance(value, bytes):\n                value = value.decode(\"latin-1\")\n            headers.append(f\"{name}: {value}\\r\\n\")\n\n        headers.append(\"\\r\\n\")\n        self.transport.write(\"\".join(headers).encode(\"latin-1\"))\n\n    async def _read_frames(self):\n        \"\"\"Read and process incoming WebSocket frames.\"\"\"\n        try:\n            while not self.closed:\n                frame = await self._read_frame()\n                if frame is None:\n                    break\n\n                opcode, payload = frame\n\n                if opcode == OPCODE_CLOSE:\n                    await self._handle_close(payload)\n                    break\n\n                if opcode == OPCODE_PING:\n                    await self._send_frame(OPCODE_PONG, payload)\n                elif opcode == OPCODE_PONG:\n                    # Ignore pongs\n                    pass\n                elif opcode == OPCODE_TEXT:\n                    await self._receive_queue.put({\n                        \"type\": \"websocket.receive\",\n                        \"text\": payload.decode(\"utf-8\"),\n                    })\n                elif opcode == OPCODE_BINARY:\n                    await self._receive_queue.put({\n                        \"type\": \"websocket.receive\",\n                        \"bytes\": payload,\n                    })\n                elif opcode == OPCODE_CONTINUATION:\n                    # Handle fragmented messages\n                    await self._handle_continuation(payload)\n\n        except asyncio.CancelledError:\n            raise\n        except Exception as e:\n            self.log.debug(\"WebSocket read error: %s\", e)\n        finally:\n            # Signal disconnect\n            if not self.closed:\n                self.closed = True\n                await self._receive_queue.put({\n                    \"type\": \"websocket.disconnect\",\n                    \"code\": self.close_code or CLOSE_ABNORMAL,\n                })\n\n    async def _read_frame(self):  # pylint: disable=too-many-return-statements\n        \"\"\"Read a single WebSocket frame.\n\n        Returns:\n            tuple: (opcode, payload) or None if connection closed\n        \"\"\"\n        # Read frame header (2 bytes minimum)\n        header = await self._read_exact(2)\n        if not header:\n            return None\n\n        first_byte, second_byte = header[0], header[1]\n\n        fin = (first_byte >> 7) & 1\n        rsv1 = (first_byte >> 6) & 1\n        rsv2 = (first_byte >> 5) & 1\n        rsv3 = (first_byte >> 4) & 1\n        opcode = first_byte & 0x0F\n\n        # RSV bits must be 0 (no extensions)\n        if rsv1 or rsv2 or rsv3:\n            await self._send_close(CLOSE_PROTOCOL_ERROR, \"RSV bits set\")\n            return None\n\n        masked = (second_byte >> 7) & 1\n        payload_len = second_byte & 0x7F\n\n        # Client frames must be masked (RFC 6455)\n        if not masked:\n            await self._send_close(CLOSE_PROTOCOL_ERROR, \"Frame not masked\")\n            return None\n\n        # Extended payload length\n        if payload_len == 126:\n            ext_len = await self._read_exact(2)\n            if not ext_len:\n                return None\n            payload_len = struct.unpack(\"!H\", ext_len)[0]\n        elif payload_len == 127:\n            ext_len = await self._read_exact(8)\n            if not ext_len:\n                return None\n            payload_len = struct.unpack(\"!Q\", ext_len)[0]\n\n        # Read masking key\n        masking_key = await self._read_exact(4)\n        if not masking_key:\n            return None\n\n        # Read payload\n        payload = await self._read_exact(payload_len)\n        if payload is None:\n            return None\n\n        # Unmask payload\n        payload = self._unmask(payload, masking_key)\n\n        # Handle fragmented messages\n        if opcode == OPCODE_CONTINUATION:\n            if self._fragment_opcode is None:\n                await self._send_close(CLOSE_PROTOCOL_ERROR, \"Unexpected continuation\")\n                return None\n            self._fragments.append(payload)\n            if fin:\n                # Reassemble complete message\n                full_payload = b\"\".join(self._fragments)\n                final_opcode = self._fragment_opcode\n                self._fragments = []\n                self._fragment_opcode = None\n                return (final_opcode, full_payload)\n            return (OPCODE_CONTINUATION, b\"\")  # Fragment received, wait for more\n        elif opcode in (OPCODE_TEXT, OPCODE_BINARY):\n            if not fin:\n                # Start of fragmented message\n                self._fragment_opcode = opcode\n                self._fragments = [payload]\n                return (OPCODE_CONTINUATION, b\"\")  # Fragment started, wait for more\n            return (opcode, payload)\n        else:\n            # Control frames\n            return (opcode, payload)\n\n    async def _read_exact(self, n):\n        \"\"\"Read exactly n bytes from the reader.\"\"\"\n        try:\n            data = await self.reader.readexactly(n)\n            return data\n        except asyncio.IncompleteReadError:\n            return None\n        except Exception:\n            return None\n\n    def _unmask(self, payload, masking_key):\n        \"\"\"Unmask WebSocket payload data.\"\"\"\n        if not payload:\n            return payload\n        # XOR each byte with corresponding mask byte\n        return bytes(b ^ masking_key[i % 4] for i, b in enumerate(payload))\n\n    async def _handle_close(self, payload):\n        \"\"\"Handle incoming close frame.\"\"\"\n        if len(payload) >= 2:\n            self.close_code = struct.unpack(\"!H\", payload[:2])[0]\n            self.close_reason = payload[2:].decode(\"utf-8\", errors=\"replace\")\n        else:\n            self.close_code = CLOSE_NO_STATUS\n            self.close_reason = \"\"\n\n        # Echo close frame back if we haven't already sent one\n        if not self.closed:\n            await self._send_close(self.close_code, self.close_reason)\n\n        self.closed = True\n\n    async def _handle_continuation(self, payload):  # pylint: disable=unused-argument\n        \"\"\"Handle continuation frame (already processed in _read_frame).\"\"\"\n        # This is called for partial fragments, nothing to do here\n\n    async def _send_frame(self, opcode, payload):\n        \"\"\"Send a WebSocket frame.\n\n        Server frames are not masked (RFC 6455).\n        \"\"\"\n        if isinstance(payload, str):\n            payload = payload.encode(\"utf-8\")\n\n        length = len(payload)\n        frame = bytearray()\n\n        # First byte: FIN + opcode\n        frame.append(0x80 | opcode)\n\n        # Second byte: length (no mask bit for server)\n        if length < 126:\n            frame.append(length)\n        elif length < 65536:\n            frame.append(126)\n            frame.extend(struct.pack(\"!H\", length))\n        else:\n            frame.append(127)\n            frame.extend(struct.pack(\"!Q\", length))\n\n        # Payload\n        frame.extend(payload)\n\n        self.transport.write(bytes(frame))\n\n    async def _send_close(self, code, reason=\"\"):\n        \"\"\"Send a close frame.\"\"\"\n        payload = struct.pack(\"!H\", code)\n        if reason:\n            payload += reason.encode(\"utf-8\")[:123]  # Max 125 bytes total\n        await self._send_frame(OPCODE_CLOSE, payload)\n        self.closed = True\n"
  },
  {
    "path": "gunicorn/config.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n# Please remember to run \"make -C docs html\" after update \"desc\" attributes.\n\nimport argparse\nimport copy\nimport grp\nimport inspect\nimport ipaddress\nimport os\nimport pwd\nimport re\nimport shlex\nimport ssl\nimport sys\nimport textwrap\n\nfrom gunicorn import __version__, util\nfrom gunicorn.errors import ConfigError\nfrom gunicorn.reloader import reloader_engines\n\nKNOWN_SETTINGS = []\nPLATFORM = sys.platform\n\n\ndef make_settings(ignore=None):\n    settings = {}\n    ignore = ignore or ()\n    for s in KNOWN_SETTINGS:\n        setting = s()\n        if setting.name in ignore:\n            continue\n        settings[setting.name] = setting.copy()\n    return settings\n\n\ndef auto_int(_, x):\n    # for compatible with octal numbers in python3\n    if re.match(r'0(\\d)', x, re.IGNORECASE):\n        x = x.replace('0', '0o', 1)\n    return int(x, 0)\n\n\nclass Config:\n\n    def __init__(self, usage=None, prog=None):\n        self.settings = make_settings()\n        self._forwarded_allow_networks = None\n        self._proxy_allow_networks = None\n        self.usage = usage\n        self.prog = prog or os.path.basename(sys.argv[0])\n        self.env_orig = os.environ.copy()\n\n    def __str__(self):\n        lines = []\n        kmax = max(len(k) for k in self.settings)\n        for k in sorted(self.settings):\n            v = self.settings[k].value\n            if callable(v):\n                v = \"<{}()>\".format(v.__qualname__)\n            lines.append(\"{k:{kmax}} = {v}\".format(k=k, v=v, kmax=kmax))\n        return \"\\n\".join(lines)\n\n    def __getattr__(self, name):\n        if name == \"settings\":\n            raise AttributeError()\n        if name not in self.settings:\n            raise AttributeError(\"No configuration setting for: %s\" % name)\n        return self.settings[name].get()\n\n    def __setattr__(self, name, value):\n        if name != \"settings\" and name in self.settings:\n            raise AttributeError(\"Invalid access!\")\n        super().__setattr__(name, value)\n\n    def set(self, name, value):\n        if name not in self.settings:\n            raise AttributeError(\"No configuration setting for: %s\" % name)\n        self.settings[name].set(value)\n\n    def get_cmd_args_from_env(self):\n        if 'GUNICORN_CMD_ARGS' in self.env_orig:\n            return shlex.split(self.env_orig['GUNICORN_CMD_ARGS'])\n        return []\n\n    def parser(self):\n        kwargs = {\n            \"usage\": self.usage,\n            \"prog\": self.prog\n        }\n        parser = argparse.ArgumentParser(**kwargs)\n        parser.add_argument(\"-v\", \"--version\",\n                            action=\"version\", default=argparse.SUPPRESS,\n                            version=\"%(prog)s (version \" + __version__ + \")\\n\",\n                            help=\"show program's version number and exit\")\n        parser.add_argument(\"args\", nargs=\"*\", help=argparse.SUPPRESS)\n\n        keys = sorted(self.settings, key=self.settings.__getitem__)\n        for k in keys:\n            self.settings[k].add_option(parser)\n\n        return parser\n\n    @property\n    def worker_class_str(self):\n        uri = self.settings['worker_class'].get()\n\n        if isinstance(uri, str):\n            # are we using a threaded worker?\n            is_sync = uri.endswith('SyncWorker') or uri == 'sync'\n            if is_sync and self.threads > 1:\n                return \"gthread\"\n            return uri\n        return uri.__name__\n\n    @property\n    def worker_class(self):\n        uri = self.settings['worker_class'].get()\n\n        # are we using a threaded worker?\n        is_sync = isinstance(uri, str) and (uri.endswith('SyncWorker') or uri == 'sync')\n        if is_sync and self.threads > 1:\n            uri = \"gunicorn.workers.gthread.ThreadWorker\"\n\n        worker_class = util.load_class(uri)\n        if hasattr(worker_class, \"setup\"):\n            worker_class.setup()\n        return worker_class\n\n    @property\n    def address(self):\n        s = self.settings['bind'].get()\n        return [util.parse_address(util.bytes_to_str(bind)) for bind in s]\n\n    @property\n    def uid(self):\n        return self.settings['user'].get()\n\n    @property\n    def gid(self):\n        return self.settings['group'].get()\n\n    @property\n    def proc_name(self):\n        pn = self.settings['proc_name'].get()\n        if pn is not None:\n            return pn\n        else:\n            return self.settings['default_proc_name'].get()\n\n    @property\n    def logger_class(self):\n        uri = self.settings['logger_class'].get()\n        if uri == \"simple\":\n            # support the default\n            uri = LoggerClass.default\n\n        # if default logger is in use, and statsd is on, automagically switch\n        # to the statsd logger\n        if uri == LoggerClass.default:\n            if 'statsd_host' in self.settings and self.settings['statsd_host'].value is not None:\n                uri = \"gunicorn.instrument.statsd.Statsd\"\n\n        logger_class = util.load_class(\n            uri,\n            default=\"gunicorn.glogging.Logger\",\n            section=\"gunicorn.loggers\")\n\n        if hasattr(logger_class, \"install\"):\n            logger_class.install()\n        return logger_class\n\n    @property\n    def is_ssl(self):\n        return self.certfile or self.keyfile\n\n    def forwarded_allow_networks(self):\n        \"\"\"Return cached network objects for forwarded_allow_ips (internal use).\"\"\"\n        if self._forwarded_allow_networks is None:\n            self._forwarded_allow_networks = [\n                ipaddress.ip_network(addr)\n                for addr in self.forwarded_allow_ips\n                if addr != \"*\"\n            ]\n        return self._forwarded_allow_networks\n\n    def proxy_allow_networks(self):\n        \"\"\"Return cached network objects for proxy_allow_ips (internal use).\"\"\"\n        if self._proxy_allow_networks is None:\n            self._proxy_allow_networks = [\n                ipaddress.ip_network(addr)\n                for addr in self.proxy_allow_ips\n                if addr != \"*\"\n            ]\n        return self._proxy_allow_networks\n\n    @property\n    def ssl_options(self):\n        opts = {}\n        for name, value in self.settings.items():\n            if value.section == 'SSL':\n                opts[name] = value.get()\n        return opts\n\n    @property\n    def env(self):\n        raw_env = self.settings['raw_env'].get()\n        env = {}\n\n        if not raw_env:\n            return env\n\n        for e in raw_env:\n            s = util.bytes_to_str(e)\n            try:\n                k, v = s.split('=', 1)\n            except ValueError:\n                raise RuntimeError(\"environment setting %r invalid\" % s)\n\n            env[k] = v\n\n        return env\n\n    @property\n    def sendfile(self):\n        if self.settings['sendfile'].get() is not None:\n            return False\n\n        if 'SENDFILE' in os.environ:\n            sendfile = os.environ['SENDFILE'].lower()\n            return sendfile in ['y', '1', 'yes', 'true']\n\n        return True\n\n    @property\n    def reuse_port(self):\n        return self.settings['reuse_port'].get()\n\n    @property\n    def paste_global_conf(self):\n        raw_global_conf = self.settings['raw_paste_global_conf'].get()\n        if raw_global_conf is None:\n            return None\n\n        global_conf = {}\n        for e in raw_global_conf:\n            s = util.bytes_to_str(e)\n            try:\n                k, v = re.split(r'(?<!\\\\)=', s, maxsplit=1)\n            except ValueError:\n                raise RuntimeError(\"environment setting %r invalid\" % s)\n            k = k.replace('\\\\=', '=')\n            v = v.replace('\\\\=', '=')\n            global_conf[k] = v\n\n        return global_conf\n\n\nclass SettingMeta(type):\n    def __new__(cls, name, bases, attrs):\n        super_new = super().__new__\n        parents = [b for b in bases if isinstance(b, SettingMeta)]\n        if not parents:\n            return super_new(cls, name, bases, attrs)\n\n        attrs[\"order\"] = len(KNOWN_SETTINGS)\n        attrs[\"validator\"] = staticmethod(attrs[\"validator\"])\n\n        new_class = super_new(cls, name, bases, attrs)\n        new_class.fmt_desc(attrs.get(\"desc\", \"\"))\n        KNOWN_SETTINGS.append(new_class)\n        return new_class\n\n    def fmt_desc(cls, desc):\n        desc = textwrap.dedent(desc).strip()\n        setattr(cls, \"desc\", desc)\n        setattr(cls, \"short\", desc.splitlines()[0])\n\n\nclass Setting:\n    name = None\n    value = None\n    section = None\n    cli = None\n    validator = None\n    type = None\n    meta = None\n    action = None\n    default = None\n    short = None\n    desc = None\n    nargs = None\n    const = None\n\n    def __init__(self):\n        if self.default is not None:\n            self.set(self.default)\n\n    def add_option(self, parser):\n        if not self.cli:\n            return\n        args = tuple(self.cli)\n\n        help_txt = \"%s [%s]\" % (self.short, self.default)\n        help_txt = help_txt.replace(\"%\", \"%%\")\n\n        kwargs = {\n            \"dest\": self.name,\n            \"action\": self.action or \"store\",\n            \"type\": self.type or str,\n            \"default\": None,\n            \"help\": help_txt\n        }\n\n        if self.meta is not None:\n            kwargs['metavar'] = self.meta\n\n        if kwargs[\"action\"] != \"store\":\n            kwargs.pop(\"type\")\n\n        if self.nargs is not None:\n            kwargs[\"nargs\"] = self.nargs\n\n        if self.const is not None:\n            kwargs[\"const\"] = self.const\n\n        parser.add_argument(*args, **kwargs)\n\n    def copy(self):\n        return copy.copy(self)\n\n    def get(self):\n        return self.value\n\n    def set(self, val):\n        if not callable(self.validator):\n            raise TypeError('Invalid validator: %s' % self.name)\n        self.value = self.validator(val)\n\n    def __lt__(self, other):\n        return (self.section == other.section and\n                self.order < other.order)\n    __cmp__ = __lt__\n\n    def __repr__(self):\n        return \"<%s.%s object at %x with value %r>\" % (\n            self.__class__.__module__,\n            self.__class__.__name__,\n            id(self),\n            self.value,\n        )\n\n\nSetting = SettingMeta('Setting', (Setting,), {})\n\n\ndef validate_bool(val):\n    if val is None:\n        return\n\n    if isinstance(val, bool):\n        return val\n    if not isinstance(val, str):\n        raise TypeError(\"Invalid type for casting: %s\" % val)\n    if val.lower().strip() == \"true\":\n        return True\n    elif val.lower().strip() == \"false\":\n        return False\n    else:\n        raise ValueError(\"Invalid boolean: %s\" % val)\n\n\ndef validate_dict(val):\n    if not isinstance(val, dict):\n        raise TypeError(\"Value is not a dictionary: %s \" % val)\n    return val\n\n\ndef validate_pos_int(val):\n    if not isinstance(val, int):\n        val = int(val, 0)\n    else:\n        # Booleans are ints!\n        val = int(val)\n    if val < 0:\n        raise ValueError(\"Value must be positive: %s\" % val)\n    return val\n\n\ndef validate_http2_frame_size(val):\n    \"\"\"Validate HTTP/2 max frame size per RFC 7540.\"\"\"\n    if not isinstance(val, int):\n        val = int(val, 0)\n    else:\n        val = int(val)\n    if val < 16384 or val > 16777215:\n        raise ValueError(\n            f\"http2_max_frame_size must be between 16384 and 16777215, got {val}\"\n        )\n    return val\n\n\ndef validate_ssl_version(val):\n    if val != SSLVersion.default:\n        sys.stderr.write(\"Warning: option `ssl_version` is deprecated and it is ignored. Use ssl_context instead.\\n\")\n    return val\n\n\ndef validate_string(val):\n    if val is None:\n        return None\n    if not isinstance(val, str):\n        raise TypeError(\"Not a string: %s\" % val)\n    return val.strip()\n\n\ndef validate_file_exists(val):\n    if val is None:\n        return None\n    if not os.path.exists(val):\n        raise ValueError(\"File %s does not exists.\" % val)\n    return val\n\n\ndef validate_list_string(val):\n    if not val:\n        return []\n\n    # legacy syntax\n    if isinstance(val, str):\n        val = [val]\n\n    return [validate_string(v) for v in val]\n\n\ndef validate_list_of_existing_files(val):\n    return [validate_file_exists(v) for v in validate_list_string(val)]\n\n\ndef validate_string_to_addr_list(val):\n    val = validate_string_to_list(val)\n\n    for addr in val:\n        if addr == \"*\":\n            continue\n        # Validate that it's a valid IP address or CIDR network\n        # but keep the string representation for backward compatibility.\n        # Use strict mode to detect mistakes like 192.168.1.1/24 where\n        # host bits are set (should be 192.168.1.0/24).\n        ipaddress.ip_network(addr)\n\n    return val\n\n\ndef validate_string_to_list(val):\n    val = validate_string(val)\n\n    if not val:\n        return []\n\n    return [v.strip() for v in val.split(\",\") if v]\n\n\ndef validate_class(val):\n    if inspect.isfunction(val) or inspect.ismethod(val):\n        val = val()\n    if inspect.isclass(val):\n        return val\n    return validate_string(val)\n\n\ndef validate_callable(arity):\n    def _validate_callable(val):\n        if isinstance(val, str):\n            try:\n                mod_name, obj_name = val.rsplit(\".\", 1)\n            except ValueError:\n                raise TypeError(\"Value '%s' is not import string. \"\n                                \"Format: module[.submodules...].object\" % val)\n            try:\n                mod = __import__(mod_name, fromlist=[obj_name])\n                val = getattr(mod, obj_name)\n            except ImportError as e:\n                raise TypeError(str(e))\n            except AttributeError:\n                raise TypeError(\"Can not load '%s' from '%s'\"\n                                \"\" % (obj_name, mod_name))\n        if not callable(val):\n            raise TypeError(\"Value is not callable: %s\" % val)\n        if arity != -1 and arity != util.get_arity(val):\n            raise TypeError(\"Value must have an arity of: %s\" % arity)\n        return val\n    return _validate_callable\n\n\ndef validate_user(val):\n    if val is None:\n        return os.geteuid()\n    if isinstance(val, int):\n        return val\n    elif val.isdigit():\n        return int(val)\n    else:\n        try:\n            return pwd.getpwnam(val).pw_uid\n        except KeyError:\n            raise ConfigError(\"No such user: '%s'\" % val)\n\n\ndef validate_group(val):\n    if val is None:\n        return os.getegid()\n\n    if isinstance(val, int):\n        return val\n    elif val.isdigit():\n        return int(val)\n    else:\n        try:\n            return grp.getgrnam(val).gr_gid\n        except KeyError:\n            raise ConfigError(\"No such group: '%s'\" % val)\n\n\ndef validate_post_request(val):\n    val = validate_callable(-1)(val)\n\n    largs = util.get_arity(val)\n    if largs == 4:\n        return val\n    elif largs == 3:\n        return lambda worker, req, env, _r: val(worker, req, env)\n    elif largs == 2:\n        return lambda worker, req, _e, _r: val(worker, req)\n    else:\n        raise TypeError(\"Value must have an arity of: 4\")\n\n\ndef validate_chdir(val):\n    # valid if the value is a string\n    val = validate_string(val)\n\n    # transform relative paths\n    path = os.path.abspath(os.path.normpath(os.path.join(util.getcwd(), val)))\n\n    # test if the path exists\n    if not os.path.exists(path):\n        raise ConfigError(\"can't chdir to %r\" % val)\n\n    return path\n\n\ndef validate_statsd_address(val):\n    val = validate_string(val)\n    if val is None:\n        return None\n\n    # As of major release 20, util.parse_address would recognize unix:PORT\n    # as a UDS address, breaking backwards compatibility. We defend against\n    # that regression here (this is also unit-tested).\n    # Feel free to remove in the next major release.\n    unix_hostname_regression = re.match(r'^unix:(\\d+)$', val)\n    if unix_hostname_regression:\n        return ('unix', int(unix_hostname_regression.group(1)))\n\n    try:\n        address = util.parse_address(val, default_port='8125')\n    except RuntimeError:\n        raise TypeError(\"Value must be one of ('host:port', 'unix://PATH')\")\n\n    return address\n\n\ndef validate_reload_engine(val):\n    if val not in reloader_engines:\n        raise ConfigError(\"Invalid reload_engine: %r\" % val)\n\n    return val\n\n\ndef get_default_config_file():\n    config_path = os.path.join(os.path.abspath(os.getcwd()),\n                               'gunicorn.conf.py')\n    if os.path.exists(config_path):\n        return config_path\n    return None\n\n\nclass ConfigFile(Setting):\n    name = \"config\"\n    section = \"Config File\"\n    cli = [\"-c\", \"--config\"]\n    meta = \"CONFIG\"\n    validator = validate_string\n    default = \"./gunicorn.conf.py\"\n    desc = \"\"\"\\\n        :ref:`The Gunicorn config file<configuration_file>`.\n\n        A string of the form ``PATH``, ``file:PATH``, or ``python:MODULE_NAME``.\n\n        Only has an effect when specified on the command line or as part of an\n        application specific configuration.\n\n        By default, a file named ``gunicorn.conf.py`` will be read from the same\n        directory where gunicorn is being run.\n\n        .. versionchanged:: 19.4\n           Loading the config from a Python module requires the ``python:``\n           prefix.\n        \"\"\"\n\n\nclass WSGIApp(Setting):\n    name = \"wsgi_app\"\n    section = \"Config File\"\n    meta = \"STRING\"\n    validator = validate_string\n    default = None\n    desc = \"\"\"\\\n        A WSGI application path in pattern ``$(MODULE_NAME):$(VARIABLE_NAME)``.\n\n        .. versionadded:: 20.1.0\n        \"\"\"\n\n\nclass Bind(Setting):\n    name = \"bind\"\n    action = \"append\"\n    section = \"Server Socket\"\n    cli = [\"-b\", \"--bind\"]\n    meta = \"ADDRESS\"\n    validator = validate_list_string\n\n    if 'PORT' in os.environ:\n        default = ['0.0.0.0:{0}'.format(os.environ.get('PORT'))]\n    else:\n        default = ['127.0.0.1:8000']\n\n    desc = \"\"\"\\\n        The socket to bind.\n\n        A string of the form: ``HOST``, ``HOST:PORT``, ``unix:PATH``,\n        ``fd://FD``. An IP is a valid ``HOST``.\n\n        .. versionchanged:: 20.0\n           Support for ``fd://FD`` got added.\n\n        Multiple addresses can be bound. ex.::\n\n            $ gunicorn -b 127.0.0.1:8000 -b [::1]:8000 test:app\n\n        will bind the `test:app` application on localhost both on ipv6\n        and ipv4 interfaces.\n\n        If the ``PORT`` environment variable is defined, the default\n        is ``['0.0.0.0:$PORT']``. If it is not defined, the default\n        is ``['127.0.0.1:8000']``.\n        \"\"\"\n\n\nclass Backlog(Setting):\n    name = \"backlog\"\n    section = \"Server Socket\"\n    cli = [\"--backlog\"]\n    meta = \"INT\"\n    validator = validate_pos_int\n    type = int\n    default = 2048\n    desc = \"\"\"\\\n        The maximum number of pending connections.\n\n        This refers to the number of clients that can be waiting to be served.\n        Exceeding this number results in the client getting an error when\n        attempting to connect. It should only affect servers under significant\n        load.\n\n        Must be a positive integer. Generally set in the 64-2048 range.\n        \"\"\"\n\n\nclass Workers(Setting):\n    name = \"workers\"\n    section = \"Worker Processes\"\n    cli = [\"-w\", \"--workers\"]\n    meta = \"INT\"\n    validator = validate_pos_int\n    type = int\n    default = int(os.environ.get(\"WEB_CONCURRENCY\", 1))\n    desc = \"\"\"\\\n        The number of worker processes for handling requests.\n\n        A positive integer generally in the ``2-4 x $(NUM_CORES)`` range.\n        You'll want to vary this a bit to find the best for your particular\n        application's work load.\n\n        By default, the value of the ``WEB_CONCURRENCY`` environment variable,\n        which is set by some Platform-as-a-Service providers such as Heroku. If\n        it is not defined, the default is ``1``.\n        \"\"\"\n\n\nclass WorkerClass(Setting):\n    name = \"worker_class\"\n    section = \"Worker Processes\"\n    cli = [\"-k\", \"--worker-class\"]\n    meta = \"STRING\"\n    validator = validate_class\n    default = \"sync\"\n    desc = \"\"\"\\\n        The type of workers to use.\n\n        The default class (``sync``) should handle most \"normal\" types of\n        workloads. You'll want to read :doc:`design` for information on when\n        you might want to choose one of the other worker classes. Required\n        libraries may be installed using setuptools' ``extras_require`` feature.\n\n        A string referring to one of the following bundled classes:\n\n        * ``sync``\n        * ``eventlet`` - **DEPRECATED: will be removed in 26.0**. Requires eventlet >= 0.40.3\n        * ``gevent``   - Requires gevent >= 24.10.1 (or install it via\n          ``pip install gunicorn[gevent]``)\n        * ``tornado``  - Requires tornado >= 6.5.0 (or install it via\n          ``pip install gunicorn[tornado]``)\n        * ``gthread``  - Python 2 requires the futures package to be installed\n          (or install it via ``pip install gunicorn[gthread]``)\n\n        Optionally, you can provide your own worker by giving Gunicorn a\n        Python path to a subclass of ``gunicorn.workers.base.Worker``.\n        This alternative syntax will load the gevent class:\n        ``gunicorn.workers.ggevent.GeventWorker``.\n        \"\"\"\n\n\nclass WorkerThreads(Setting):\n    name = \"threads\"\n    section = \"Worker Processes\"\n    cli = [\"--threads\"]\n    meta = \"INT\"\n    validator = validate_pos_int\n    type = int\n    default = 1\n    desc = \"\"\"\\\n        The number of worker threads for handling requests.\n\n        Run each worker with the specified number of threads.\n\n        A positive integer generally in the ``2-4 x $(NUM_CORES)`` range.\n        You'll want to vary this a bit to find the best for your particular\n        application's work load.\n\n        If it is not defined, the default is ``1``.\n\n        This setting only affects the Gthread worker type.\n\n        .. note::\n           If you try to use the ``sync`` worker type and set the ``threads``\n           setting to more than 1, the ``gthread`` worker type will be used\n           instead.\n        \"\"\"\n\n\nclass WorkerConnections(Setting):\n    name = \"worker_connections\"\n    section = \"Worker Processes\"\n    cli = [\"--worker-connections\"]\n    meta = \"INT\"\n    validator = validate_pos_int\n    type = int\n    default = 1000\n    desc = \"\"\"\\\n        The maximum number of simultaneous clients.\n\n        This setting only affects the ``gthread``, ``eventlet`` and ``gevent`` worker types.\n        \"\"\"\n\n\nclass MaxRequests(Setting):\n    name = \"max_requests\"\n    section = \"Worker Processes\"\n    cli = [\"--max-requests\"]\n    meta = \"INT\"\n    validator = validate_pos_int\n    type = int\n    default = 0\n    desc = \"\"\"\\\n        The maximum number of requests a worker will process before restarting.\n\n        Any value greater than zero will limit the number of requests a worker\n        will process before automatically restarting. This is a simple method\n        to help limit the damage of memory leaks.\n\n        If this is set to zero (the default) then the automatic worker\n        restarts are disabled.\n        \"\"\"\n\n\nclass MaxRequestsJitter(Setting):\n    name = \"max_requests_jitter\"\n    section = \"Worker Processes\"\n    cli = [\"--max-requests-jitter\"]\n    meta = \"INT\"\n    validator = validate_pos_int\n    type = int\n    default = 0\n    desc = \"\"\"\\\n        The maximum jitter to add to the *max_requests* setting.\n\n        The jitter causes the restart per worker to be randomized by\n        ``randint(0, max_requests_jitter)``. This is intended to stagger worker\n        restarts to avoid all workers restarting at the same time.\n\n        .. versionadded:: 19.2\n        \"\"\"\n\n\nclass Timeout(Setting):\n    name = \"timeout\"\n    section = \"Worker Processes\"\n    cli = [\"-t\", \"--timeout\"]\n    meta = \"INT\"\n    validator = validate_pos_int\n    type = int\n    default = 30\n    desc = \"\"\"\\\n        Workers silent for more than this many seconds are killed and restarted.\n\n        Value is a positive number or 0. Setting it to 0 has the effect of\n        infinite timeouts by disabling timeouts for all workers entirely.\n\n        Generally, the default of thirty seconds should suffice. Only set this\n        noticeably higher if you're sure of the repercussions for sync workers.\n        For the non sync workers it just means that the worker process is still\n        communicating and is not tied to the length of time required to handle a\n        single request.\n        \"\"\"\n\n\nclass GracefulTimeout(Setting):\n    name = \"graceful_timeout\"\n    section = \"Worker Processes\"\n    cli = [\"--graceful-timeout\"]\n    meta = \"INT\"\n    validator = validate_pos_int\n    type = int\n    default = 30\n    desc = \"\"\"\\\n        Timeout for graceful workers restart in seconds.\n\n        After receiving a restart signal, workers have this much time to finish\n        serving requests. Workers still alive after the timeout (starting from\n        the receipt of the restart signal) are force killed.\n        \"\"\"\n\n\nclass Keepalive(Setting):\n    name = \"keepalive\"\n    section = \"Worker Processes\"\n    cli = [\"--keep-alive\"]\n    meta = \"INT\"\n    validator = validate_pos_int\n    type = int\n    default = 2\n    desc = \"\"\"\\\n        The number of seconds to wait for requests on a Keep-Alive connection.\n\n        Generally set in the 1-5 seconds range for servers with direct connection\n        to the client (e.g. when you don't have separate load balancer). When\n        Gunicorn is deployed behind a load balancer, it often makes sense to\n        set this to a higher value.\n\n        .. note::\n           ``sync`` worker does not support persistent connections and will\n           ignore this option.\n        \"\"\"\n\n\nclass LimitRequestLine(Setting):\n    name = \"limit_request_line\"\n    section = \"Security\"\n    cli = [\"--limit-request-line\"]\n    meta = \"INT\"\n    validator = validate_pos_int\n    type = int\n    default = 4094\n    desc = \"\"\"\\\n        The maximum size of HTTP request line in bytes.\n\n        This parameter is used to limit the allowed size of a client's\n        HTTP request-line. Since the request-line consists of the HTTP\n        method, URI, and protocol version, this directive places a\n        restriction on the length of a request-URI allowed for a request\n        on the server. A server needs this value to be large enough to\n        hold any of its resource names, including any information that\n        might be passed in the query part of a GET request. Value is a number\n        from 0 (unlimited) to 8190.\n\n        This parameter can be used to prevent any DDOS attack.\n        \"\"\"\n\n\nclass LimitRequestFields(Setting):\n    name = \"limit_request_fields\"\n    section = \"Security\"\n    cli = [\"--limit-request-fields\"]\n    meta = \"INT\"\n    validator = validate_pos_int\n    type = int\n    default = 100\n    desc = \"\"\"\\\n        Limit the number of HTTP headers fields in a request.\n\n        This parameter is used to limit the number of headers in a request to\n        prevent DDOS attack. Used with the *limit_request_field_size* it allows\n        more safety. By default this value is 100 and can't be larger than\n        32768.\n        \"\"\"\n\n\nclass LimitRequestFieldSize(Setting):\n    name = \"limit_request_field_size\"\n    section = \"Security\"\n    cli = [\"--limit-request-field_size\"]\n    meta = \"INT\"\n    validator = validate_pos_int\n    type = int\n    default = 8190\n    desc = \"\"\"\\\n        Limit the allowed size of an HTTP request header field.\n\n        Value is a positive number or 0. Setting it to 0 will allow unlimited\n        header field sizes.\n\n        .. warning::\n           Setting this parameter to a very high or unlimited value can open\n           up for DDOS attacks.\n        \"\"\"\n\n\nclass Reload(Setting):\n    name = \"reload\"\n    section = 'Debugging'\n    cli = ['--reload']\n    validator = validate_bool\n    action = 'store_true'\n    default = False\n\n    desc = '''\\\n        Restart workers when code changes.\n\n        This setting is intended for development. It will cause workers to be\n        restarted whenever application code changes.\n\n        The reloader is incompatible with application preloading. When using a\n        paste configuration be sure that the server block does not import any\n        application code or the reload will not work as designed.\n\n        The default behavior is to attempt inotify with a fallback to file\n        system polling. Generally, inotify should be preferred if available\n        because it consumes less system resources.\n\n        .. note::\n           In order to use the inotify reloader, you must have the ``inotify``\n           package installed.\n        .. warning::\n           Enabling this will change what happens on failure to load the\n           the application: While the reloader is active, any and all clients\n           that can make requests can see the full exception and traceback!\n        '''\n\n\nclass ReloadEngine(Setting):\n    name = \"reload_engine\"\n    section = \"Debugging\"\n    cli = [\"--reload-engine\"]\n    meta = \"STRING\"\n    validator = validate_reload_engine\n    default = \"auto\"\n    desc = \"\"\"\\\n        The implementation that should be used to power :ref:`reload`.\n\n        Valid engines are:\n\n        * ``'auto'``\n        * ``'poll'``\n        * ``'inotify'`` (requires inotify)\n\n        .. versionadded:: 19.7\n        \"\"\"\n\n\nclass ReloadExtraFiles(Setting):\n    name = \"reload_extra_files\"\n    action = \"append\"\n    section = \"Debugging\"\n    cli = [\"--reload-extra-file\"]\n    meta = \"FILES\"\n    validator = validate_list_of_existing_files\n    default = []\n    desc = \"\"\"\\\n        Extends :ref:`reload` option to also watch and reload on additional files\n        (e.g., templates, configurations, specifications, etc.).\n\n        .. versionadded:: 19.8\n        \"\"\"\n\n\nclass Spew(Setting):\n    name = \"spew\"\n    section = \"Debugging\"\n    cli = [\"--spew\"]\n    validator = validate_bool\n    action = \"store_true\"\n    default = False\n    desc = \"\"\"\\\n        Install a trace function that spews every line executed by the server.\n\n        This is the nuclear option.\n        \"\"\"\n\n\nclass ConfigCheck(Setting):\n    name = \"check_config\"\n    section = \"Debugging\"\n    cli = [\"--check-config\"]\n    validator = validate_bool\n    action = \"store_true\"\n    default = False\n    desc = \"\"\"\\\n        Check the configuration and exit. The exit status is 0 if the\n        configuration is correct, and 1 if the configuration is incorrect.\n        \"\"\"\n\n\nclass PrintConfig(Setting):\n    name = \"print_config\"\n    section = \"Debugging\"\n    cli = [\"--print-config\"]\n    validator = validate_bool\n    action = \"store_true\"\n    default = False\n    desc = \"\"\"\\\n        Print the configuration settings as fully resolved. Implies :ref:`check-config`.\n        \"\"\"\n\n\nclass PreloadApp(Setting):\n    name = \"preload_app\"\n    section = \"Server Mechanics\"\n    cli = [\"--preload\"]\n    validator = validate_bool\n    action = \"store_true\"\n    default = False\n    desc = \"\"\"\\\n        Load application code before the worker processes are forked.\n\n        By preloading an application you can save some RAM resources as well as\n        speed up server boot times. Although, if you defer application loading\n        to each worker process, you can reload your application code easily by\n        restarting workers.\n        \"\"\"\n\n\nclass Sendfile(Setting):\n    name = \"sendfile\"\n    section = \"Server Mechanics\"\n    cli = [\"--no-sendfile\"]\n    validator = validate_bool\n    action = \"store_const\"\n    const = False\n\n    desc = \"\"\"\\\n        Disables the use of ``sendfile()``.\n\n        If not set, the value of the ``SENDFILE`` environment variable is used\n        to enable or disable its usage.\n\n        .. versionadded:: 19.2\n        .. versionchanged:: 19.4\n           Swapped ``--sendfile`` with ``--no-sendfile`` to actually allow\n           disabling.\n        .. versionchanged:: 19.6\n           added support for the ``SENDFILE`` environment variable\n        \"\"\"\n\n\nclass ReusePort(Setting):\n    name = \"reuse_port\"\n    section = \"Server Mechanics\"\n    cli = [\"--reuse-port\"]\n    validator = validate_bool\n    action = \"store_true\"\n    default = False\n\n    desc = \"\"\"\\\n        Set the ``SO_REUSEPORT`` flag on the listening socket.\n\n        .. versionadded:: 19.8\n        \"\"\"\n\n\nclass Chdir(Setting):\n    name = \"chdir\"\n    section = \"Server Mechanics\"\n    cli = [\"--chdir\"]\n    validator = validate_chdir\n    default = util.getcwd()\n    default_doc = \"``'.'``\"\n    desc = \"\"\"\\\n        Change directory to specified directory before loading apps.\n        \"\"\"\n\n\nclass Daemon(Setting):\n    name = \"daemon\"\n    section = \"Server Mechanics\"\n    cli = [\"-D\", \"--daemon\"]\n    validator = validate_bool\n    action = \"store_true\"\n    default = False\n    desc = \"\"\"\\\n        Daemonize the Gunicorn process.\n\n        Detaches the server from the controlling terminal and enters the\n        background.\n        \"\"\"\n\n\nclass Env(Setting):\n    name = \"raw_env\"\n    action = \"append\"\n    section = \"Server Mechanics\"\n    cli = [\"-e\", \"--env\"]\n    meta = \"ENV\"\n    validator = validate_list_string\n    default = []\n\n    desc = \"\"\"\\\n        Set environment variables in the execution environment.\n\n        Should be a list of strings in the ``key=value`` format.\n\n        For example on the command line:\n\n        .. code-block:: console\n\n            $ gunicorn -b 127.0.0.1:8000 --env FOO=1 test:app\n\n        Or in the configuration file:\n\n        .. code-block:: python\n\n            raw_env = [\"FOO=1\"]\n        \"\"\"\n\n\nclass Pidfile(Setting):\n    name = \"pidfile\"\n    section = \"Server Mechanics\"\n    cli = [\"-p\", \"--pid\"]\n    meta = \"FILE\"\n    validator = validate_string\n    default = None\n    desc = \"\"\"\\\n        A filename to use for the PID file.\n\n        If not set, no PID file will be written.\n        \"\"\"\n\n\nclass WorkerTmpDir(Setting):\n    name = \"worker_tmp_dir\"\n    section = \"Server Mechanics\"\n    cli = [\"--worker-tmp-dir\"]\n    meta = \"DIR\"\n    validator = validate_string\n    default = None\n    desc = \"\"\"\\\n        A directory to use for the worker heartbeat temporary file.\n\n        If not set, the default temporary directory will be used.\n\n        .. note::\n           The current heartbeat system involves calling ``os.fchmod`` on\n           temporary file handlers and may block a worker for arbitrary time\n           if the directory is on a disk-backed filesystem.\n\n           See :ref:`blocking-os-fchmod` for more detailed information\n           and a solution for avoiding this problem.\n        \"\"\"\n\n\nclass User(Setting):\n    name = \"user\"\n    section = \"Server Mechanics\"\n    cli = [\"-u\", \"--user\"]\n    meta = \"USER\"\n    validator = validate_user\n    default = os.geteuid()\n    default_doc = \"``os.geteuid()``\"\n    desc = \"\"\"\\\n        Switch worker processes to run as this user.\n\n        A valid user id (as an integer) or the name of a user that can be\n        retrieved with a call to ``pwd.getpwnam(value)`` or ``None`` to not\n        change the worker process user.\n        \"\"\"\n\n\nclass Group(Setting):\n    name = \"group\"\n    section = \"Server Mechanics\"\n    cli = [\"-g\", \"--group\"]\n    meta = \"GROUP\"\n    validator = validate_group\n    default = os.getegid()\n    default_doc = \"``os.getegid()``\"\n    desc = \"\"\"\\\n        Switch worker process to run as this group.\n\n        A valid group id (as an integer) or the name of a user that can be\n        retrieved with a call to ``grp.getgrnam(value)`` or ``None`` to not\n        change the worker processes group.\n        \"\"\"\n\n\nclass Umask(Setting):\n    name = \"umask\"\n    section = \"Server Mechanics\"\n    cli = [\"-m\", \"--umask\"]\n    meta = \"INT\"\n    validator = validate_pos_int\n    type = auto_int\n    default = 0\n    desc = \"\"\"\\\n        A bit mask for the file mode on files written by Gunicorn.\n\n        Note that this affects unix socket permissions.\n\n        A valid value for the ``os.umask(mode)`` call or a string compatible\n        with ``int(value, 0)`` (``0`` means Python guesses the base, so values\n        like ``0``, ``0xFF``, ``0022`` are valid for decimal, hex, and octal\n        representations)\n        \"\"\"\n\n\nclass Initgroups(Setting):\n    name = \"initgroups\"\n    section = \"Server Mechanics\"\n    cli = [\"--initgroups\"]\n    validator = validate_bool\n    action = 'store_true'\n    default = False\n\n    desc = \"\"\"\\\n        If true, set the worker process's group access list with all of the\n        groups of which the specified username is a member, plus the specified\n        group id.\n\n        .. versionadded:: 19.7\n        \"\"\"\n\n\nclass TmpUploadDir(Setting):\n    name = \"tmp_upload_dir\"\n    section = \"Server Mechanics\"\n    meta = \"DIR\"\n    validator = validate_string\n    default = None\n    desc = \"\"\"\\\n        Directory to store temporary request data as they are read.\n\n        This may disappear in the near future.\n\n        This path should be writable by the process permissions set for Gunicorn\n        workers. If not specified, Gunicorn will choose a system generated\n        temporary directory.\n        \"\"\"\n\n\nclass SecureSchemeHeader(Setting):\n    name = \"secure_scheme_headers\"\n    section = \"Server Mechanics\"\n    validator = validate_dict\n    default = {\n        \"X-FORWARDED-PROTOCOL\": \"ssl\",\n        \"X-FORWARDED-PROTO\": \"https\",\n        \"X-FORWARDED-SSL\": \"on\"\n    }\n    desc = \"\"\"\\\n\n        A dictionary containing headers and values that the front-end proxy\n        uses to indicate HTTPS requests. If the source IP is permitted by\n        :ref:`forwarded-allow-ips` (below), *and* at least one request header matches\n        a key-value pair listed in this dictionary, then Gunicorn will set\n        ``wsgi.url_scheme`` to ``https``, so your application can tell that the\n        request is secure.\n\n        If the other headers listed in this dictionary are not present in the request, they will be ignored,\n        but if the other headers are present and do not match the provided values, then\n        the request will fail to parse. See the note below for more detailed examples of this behaviour.\n\n        The dictionary should map upper-case header names to exact string\n        values. The value comparisons are case-sensitive, unlike the header\n        names, so make sure they're exactly what your front-end proxy sends\n        when handling HTTPS requests.\n\n        It is important that your front-end proxy configuration ensures that\n        the headers defined here can not be passed directly from the client.\n        \"\"\"\n\n\nclass ForwardedAllowIPS(Setting):\n    name = \"forwarded_allow_ips\"\n    section = \"Server Mechanics\"\n    cli = [\"--forwarded-allow-ips\"]\n    meta = \"STRING\"\n    validator = validate_string_to_addr_list\n    default = os.environ.get(\"FORWARDED_ALLOW_IPS\", \"127.0.0.1,::1\")\n    desc = \"\"\"\\\n        Front-end's IP addresses or networks from which allowed to handle\n        set secure headers. (comma separated).\n\n        Supports both individual IP addresses (e.g., ``192.168.1.1``) and\n        CIDR networks (e.g., ``192.168.0.0/16``).\n\n        Set to ``*`` to disable checking of front-end IPs. This is useful for setups\n        where you don't know in advance the IP address of front-end, but\n        instead have ensured via other means that only your\n        authorized front-ends can access Gunicorn.\n\n        By default, the value of the ``FORWARDED_ALLOW_IPS`` environment\n        variable. If it is not defined, the default is ``\"127.0.0.1,::1\"``.\n\n        .. note::\n\n            This option does not affect UNIX socket connections. Connections not associated with\n            an IP address are treated as allowed, unconditionally.\n\n        .. note::\n\n            The interplay between the request headers, the value of ``forwarded_allow_ips``, and the value of\n            ``secure_scheme_headers`` is complex. Various scenarios are documented below to further elaborate.\n            In each case, we have a request from the remote address 134.213.44.18, and the default value of\n            ``secure_scheme_headers``:\n\n            ```python\n            secure_scheme_headers = {\n                'X-FORWARDED-PROTOCOL': 'ssl',\n                'X-FORWARDED-PROTO': 'https',\n                'X-FORWARDED-SSL': 'on'\n            }\n            ```\n\n            +---------------------+----------------------------+-----------------------------+-------------------------+\n            | forwarded-allow-ips | Secure Request Headers     | Result                      | Explanation             |\n            +=====================+============================+=============================+=========================+\n            | `\"127.0.0.1\"`       | `X-Forwarded-Proto: https` | `wsgi.url_scheme = \"http\"`  | IP address was not      |\n            |                     |                            |                             | allowed                 |\n            +---------------------+----------------------------+-----------------------------+-------------------------+\n            |                     |                            |                             | IP address allowed, but |\n            | `\"*\"`               | `<none>`                   | `wsgi.url_scheme = \"http\"`  | no secure headers       |\n            |                     |                            |                             | provided                |\n            +---------------------+----------------------------+-----------------------------+-------------------------+\n            | `\"*\"`               | `X-Forwarded-Proto: https` | `wsgi.url_scheme = \"https\"` | IP address allowed, one |\n            |                     |                            |                             | request header matched  |\n            +---------------------+----------------------------+-----------------------------+-------------------------+\n            |                     |                            |                             | IP address allowed, but |\n            | `\"134.213.44.18\"`   | `X-Forwarded-Ssl: on`      | `InvalidSchemeHeaders()`    | the two secure headers  |\n            |                     | `X-Forwarded-Proto: http`  | raised                      | disagreed on if HTTPS   |\n            |                     |                            |                             | was used                |\n            +---------------------+----------------------------+-----------------------------+-------------------------+\n\n\n        \"\"\"\n\n\nclass AccessLog(Setting):\n    name = \"accesslog\"\n    section = \"Logging\"\n    cli = [\"--access-logfile\"]\n    meta = \"FILE\"\n    validator = validate_string\n    default = None\n    desc = \"\"\"\\\n        The Access log file to write to.\n\n        ``'-'`` means log to stdout.\n        \"\"\"\n\n\nclass DisableRedirectAccessToSyslog(Setting):\n    name = \"disable_redirect_access_to_syslog\"\n    section = \"Logging\"\n    cli = [\"--disable-redirect-access-to-syslog\"]\n    validator = validate_bool\n    action = 'store_true'\n    default = False\n    desc = \"\"\"\\\n    Disable redirect access logs to syslog.\n\n    .. versionadded:: 19.8\n    \"\"\"\n\n\nclass AccessLogFormat(Setting):\n    name = \"access_log_format\"\n    section = \"Logging\"\n    cli = [\"--access-logformat\"]\n    meta = \"STRING\"\n    validator = validate_string\n    default = '%(h)s %(l)s %(u)s %(t)s \"%(r)s\" %(s)s %(b)s \"%(f)s\" \"%(a)s\"'\n    desc = \"\"\"\\\n        The access log format.\n\n        ===========  ===========\n        Identifier   Description\n        ===========  ===========\n        h            remote address\n        l            ``'-'``\n        u            user name (if HTTP Basic auth used)\n        t            date of the request\n        r            status line (e.g. ``GET / HTTP/1.1``)\n        m            request method\n        U            URL path without query string\n        q            query string\n        H            protocol\n        s            status\n        B            response length\n        b            response length or ``'-'`` (CLF format)\n        f            referrer (note: header is ``referer``)\n        a            user agent\n        T            request time in seconds\n        M            request time in milliseconds\n        D            request time in microseconds\n        L            request time in decimal seconds\n        p            process ID\n        {header}i    request header\n        {header}o    response header\n        {variable}e  environment variable\n        ===========  ===========\n\n        Use lowercase for header and environment variable names, and put\n        ``{...}x`` names inside ``%(...)s``. For example::\n\n            %({x-forwarded-for}i)s\n        \"\"\"\n\n\nclass ErrorLog(Setting):\n    name = \"errorlog\"\n    section = \"Logging\"\n    cli = [\"--error-logfile\", \"--log-file\"]\n    meta = \"FILE\"\n    validator = validate_string\n    default = '-'\n    desc = \"\"\"\\\n        The Error log file to write to.\n\n        Using ``'-'`` for FILE makes gunicorn log to stderr.\n\n        .. versionchanged:: 19.2\n           Log to stderr by default.\n\n        \"\"\"\n\n\nclass Loglevel(Setting):\n    name = \"loglevel\"\n    section = \"Logging\"\n    cli = [\"--log-level\"]\n    meta = \"LEVEL\"\n    validator = validate_string\n    default = \"info\"\n    desc = \"\"\"\\\n        The granularity of Error log outputs.\n\n        Valid level names are:\n\n        * ``'debug'``\n        * ``'info'``\n        * ``'warning'``\n        * ``'error'``\n        * ``'critical'``\n        \"\"\"\n\n\nclass CaptureOutput(Setting):\n    name = \"capture_output\"\n    section = \"Logging\"\n    cli = [\"--capture-output\"]\n    validator = validate_bool\n    action = 'store_true'\n    default = False\n    desc = \"\"\"\\\n        Redirect stdout/stderr to specified file in :ref:`errorlog`.\n\n        .. versionadded:: 19.6\n        \"\"\"\n\n\nclass LoggerClass(Setting):\n    name = \"logger_class\"\n    section = \"Logging\"\n    cli = [\"--logger-class\"]\n    meta = \"STRING\"\n    validator = validate_class\n    default = \"gunicorn.glogging.Logger\"\n    desc = \"\"\"\\\n        The logger you want to use to log events in Gunicorn.\n\n        The default class (``gunicorn.glogging.Logger``) handles most\n        normal usages in logging. It provides error and access logging.\n\n        You can provide your own logger by giving Gunicorn a Python path to a\n        class that quacks like ``gunicorn.glogging.Logger``.\n        \"\"\"\n\n\nclass LogConfig(Setting):\n    name = \"logconfig\"\n    section = \"Logging\"\n    cli = [\"--log-config\"]\n    meta = \"FILE\"\n    validator = validate_string\n    default = None\n    desc = \"\"\"\\\n    The log config file to use.\n    Gunicorn uses the standard Python logging module's Configuration\n    file format.\n    \"\"\"\n\n\nclass LogConfigDict(Setting):\n    name = \"logconfig_dict\"\n    section = \"Logging\"\n    validator = validate_dict\n    default = {}\n    desc = \"\"\"\\\n    The log config dictionary to use, using the standard Python\n    logging module's dictionary configuration format. This option\n    takes precedence over the :ref:`logconfig` and :ref:`logconfig-json` options,\n    which uses the older file configuration format and JSON\n    respectively.\n\n    Format: https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig\n\n    For more context you can look at the default configuration dictionary for logging,\n    which can be found at ``gunicorn.glogging.CONFIG_DEFAULTS``.\n\n    .. versionadded:: 19.8\n    \"\"\"\n\n\nclass LogConfigJson(Setting):\n    name = \"logconfig_json\"\n    section = \"Logging\"\n    cli = [\"--log-config-json\"]\n    meta = \"FILE\"\n    validator = validate_string\n    default = None\n    desc = \"\"\"\\\n    The log config to read config from a JSON file\n\n    Format: https://docs.python.org/3/library/logging.config.html#logging.config.jsonConfig\n\n    .. versionadded:: 20.0\n    \"\"\"\n\n\nclass SyslogTo(Setting):\n    name = \"syslog_addr\"\n    section = \"Logging\"\n    cli = [\"--log-syslog-to\"]\n    meta = \"SYSLOG_ADDR\"\n    validator = validate_string\n\n    if PLATFORM == \"darwin\":\n        default = \"unix:///var/run/syslog\"\n    elif PLATFORM in ('freebsd', 'dragonfly', ):\n        default = \"unix:///var/run/log\"\n    elif PLATFORM == \"openbsd\":\n        default = \"unix:///dev/log\"\n    else:\n        default = \"udp://localhost:514\"\n\n    default_doc = \"\"\"\\\n    Platform-specific:\n\n    * macOS: ``'unix:///var/run/syslog'``\n    * FreeBSD/DragonFly: ``'unix:///var/run/log'``\n    * OpenBSD: ``'unix:///dev/log'``\n    * Linux/other: ``'udp://localhost:514'``\n    \"\"\"\n\n    desc = \"\"\"\\\n    Address to send syslog messages.\n\n    Address is a string of the form:\n\n    * ``unix://PATH#TYPE`` : for unix domain socket. ``TYPE`` can be ``stream``\n      for the stream driver or ``dgram`` for the dgram driver.\n      ``stream`` is the default.\n    * ``udp://HOST:PORT`` : for UDP sockets\n    * ``tcp://HOST:PORT`` : for TCP sockets\n\n    \"\"\"\n\n\nclass Syslog(Setting):\n    name = \"syslog\"\n    section = \"Logging\"\n    cli = [\"--log-syslog\"]\n    validator = validate_bool\n    action = 'store_true'\n    default = False\n    desc = \"\"\"\\\n    Send *Gunicorn* logs to syslog.\n\n    .. versionchanged:: 19.8\n       You can now disable sending access logs by using the\n       :ref:`disable-redirect-access-to-syslog` setting.\n    \"\"\"\n\n\nclass SyslogPrefix(Setting):\n    name = \"syslog_prefix\"\n    section = \"Logging\"\n    cli = [\"--log-syslog-prefix\"]\n    meta = \"SYSLOG_PREFIX\"\n    validator = validate_string\n    default = None\n    desc = \"\"\"\\\n    Makes Gunicorn use the parameter as program-name in the syslog entries.\n\n    All entries will be prefixed by ``gunicorn.<prefix>``. By default the\n    program name is the name of the process.\n    \"\"\"\n\n\nclass SyslogFacility(Setting):\n    name = \"syslog_facility\"\n    section = \"Logging\"\n    cli = [\"--log-syslog-facility\"]\n    meta = \"SYSLOG_FACILITY\"\n    validator = validate_string\n    default = \"user\"\n    desc = \"\"\"\\\n    Syslog facility name\n    \"\"\"\n\n\nclass EnableStdioInheritance(Setting):\n    name = \"enable_stdio_inheritance\"\n    section = \"Logging\"\n    cli = [\"-R\", \"--enable-stdio-inheritance\"]\n    validator = validate_bool\n    default = False\n    action = \"store_true\"\n    desc = \"\"\"\\\n    Enable stdio inheritance.\n\n    Enable inheritance for stdio file descriptors in daemon mode.\n\n    Note: To disable the Python stdout buffering, you can to set the user\n    environment variable ``PYTHONUNBUFFERED`` .\n    \"\"\"\n\n\n# statsD monitoring\nclass StatsdHost(Setting):\n    name = \"statsd_host\"\n    section = \"Logging\"\n    cli = [\"--statsd-host\"]\n    meta = \"STATSD_ADDR\"\n    default = None\n    validator = validate_statsd_address\n    desc = \"\"\"\\\n    The address of the StatsD server to log to.\n\n    Address is a string of the form:\n\n    * ``unix://PATH`` : for a unix domain socket.\n    * ``HOST:PORT`` : for a network address\n\n    .. versionadded:: 19.1\n    \"\"\"\n\n\n# Datadog Statsd (dogstatsd) tags. https://docs.datadoghq.com/developers/dogstatsd/\nclass DogstatsdTags(Setting):\n    name = \"dogstatsd_tags\"\n    section = \"Logging\"\n    cli = [\"--dogstatsd-tags\"]\n    meta = \"DOGSTATSD_TAGS\"\n    default = \"\"\n    validator = validate_string\n    desc = \"\"\"\\\n    A comma-delimited list of datadog statsd (dogstatsd) tags to append to\n    statsd metrics. e.g. ``'tag1:value1,tag2:value2'``\n\n    .. versionadded:: 20\n    \"\"\"\n\n\nclass StatsdPrefix(Setting):\n    name = \"statsd_prefix\"\n    section = \"Logging\"\n    cli = [\"--statsd-prefix\"]\n    meta = \"STATSD_PREFIX\"\n    default = \"\"\n    validator = validate_string\n    desc = \"\"\"\\\n    Prefix to use when emitting statsd metrics (a trailing ``.`` is added,\n    if not provided).\n\n    .. versionadded:: 19.2\n    \"\"\"\n\n\nclass BacklogMetric(Setting):\n    name = \"enable_backlog_metric\"\n    section = \"Logging\"\n    cli = [\"--enable-backlog-metric\"]\n    validator = validate_bool\n    default = False\n    action = \"store_true\"\n    desc = \"\"\"\\\n    Enable socket backlog metric (only supported on Linux).\n\n    When enabled, gunicorn will emit a ``gunicorn.backlog`` histogram metric\n    showing the number of connections waiting in the socket backlog.\n    \"\"\"\n\n\nclass Procname(Setting):\n    name = \"proc_name\"\n    section = \"Process Naming\"\n    cli = [\"-n\", \"--name\"]\n    meta = \"STRING\"\n    validator = validate_string\n    default = None\n    desc = \"\"\"\\\n        A base to use with setproctitle for process naming.\n\n        This affects things like ``ps`` and ``top``. If you're going to be\n        running more than one instance of Gunicorn you'll probably want to set a\n        name to tell them apart. This requires that you install the setproctitle\n        module.\n\n        If not set, the *default_proc_name* setting will be used.\n        \"\"\"\n\n\nclass DefaultProcName(Setting):\n    name = \"default_proc_name\"\n    section = \"Process Naming\"\n    validator = validate_string\n    default = \"gunicorn\"\n    desc = \"\"\"\\\n        Internal setting that is adjusted for each type of application.\n        \"\"\"\n\n\nclass PythonPath(Setting):\n    name = \"pythonpath\"\n    section = \"Server Mechanics\"\n    cli = [\"--pythonpath\"]\n    meta = \"STRING\"\n    validator = validate_string\n    default = None\n    desc = \"\"\"\\\n        A comma-separated list of directories to add to the Python path.\n\n        e.g.\n        ``'/home/djangoprojects/myproject,/home/python/mylibrary'``.\n        \"\"\"\n\n\nclass Paste(Setting):\n    name = \"paste\"\n    section = \"Server Mechanics\"\n    cli = [\"--paste\", \"--paster\"]\n    meta = \"STRING\"\n    validator = validate_string\n    default = None\n    desc = \"\"\"\\\n        Load a PasteDeploy config file. The argument may contain a ``#``\n        symbol followed by the name of an app section from the config file,\n        e.g. ``production.ini#admin``.\n\n        At this time, using alternate server blocks is not supported. Use the\n        command line arguments to control server configuration instead.\n        \"\"\"\n\n\nclass OnStarting(Setting):\n    name = \"on_starting\"\n    section = \"Server Hooks\"\n    validator = validate_callable(1)\n    type = callable\n\n    def on_starting(server):\n        pass\n    default = staticmethod(on_starting)\n    desc = \"\"\"\\\n        Called just before the master process is initialized.\n\n        The callable needs to accept a single instance variable for the Arbiter.\n        \"\"\"\n\n\nclass OnReload(Setting):\n    name = \"on_reload\"\n    section = \"Server Hooks\"\n    validator = validate_callable(1)\n    type = callable\n\n    def on_reload(server):\n        pass\n    default = staticmethod(on_reload)\n    desc = \"\"\"\\\n        Called to recycle workers during a reload via SIGHUP.\n\n        The callable needs to accept a single instance variable for the Arbiter.\n        \"\"\"\n\n\nclass WhenReady(Setting):\n    name = \"when_ready\"\n    section = \"Server Hooks\"\n    validator = validate_callable(1)\n    type = callable\n\n    def when_ready(server):\n        pass\n    default = staticmethod(when_ready)\n    desc = \"\"\"\\\n        Called just after the server is started.\n\n        The callable needs to accept a single instance variable for the Arbiter.\n        \"\"\"\n\n\nclass Prefork(Setting):\n    name = \"pre_fork\"\n    section = \"Server Hooks\"\n    validator = validate_callable(2)\n    type = callable\n\n    def pre_fork(server, worker):\n        pass\n    default = staticmethod(pre_fork)\n    desc = \"\"\"\\\n        Called just before a worker is forked.\n\n        The callable needs to accept two instance variables for the Arbiter and\n        new Worker.\n        \"\"\"\n\n\nclass Postfork(Setting):\n    name = \"post_fork\"\n    section = \"Server Hooks\"\n    validator = validate_callable(2)\n    type = callable\n\n    def post_fork(server, worker):\n        pass\n    default = staticmethod(post_fork)\n    desc = \"\"\"\\\n        Called just after a worker has been forked.\n\n        The callable needs to accept two instance variables for the Arbiter and\n        new Worker.\n        \"\"\"\n\n\nclass PostWorkerInit(Setting):\n    name = \"post_worker_init\"\n    section = \"Server Hooks\"\n    validator = validate_callable(1)\n    type = callable\n\n    def post_worker_init(worker):\n        pass\n\n    default = staticmethod(post_worker_init)\n    desc = \"\"\"\\\n        Called just after a worker has initialized the application.\n\n        The callable needs to accept one instance variable for the initialized\n        Worker.\n        \"\"\"\n\n\nclass WorkerInt(Setting):\n    name = \"worker_int\"\n    section = \"Server Hooks\"\n    validator = validate_callable(1)\n    type = callable\n\n    def worker_int(worker):\n        pass\n\n    default = staticmethod(worker_int)\n    desc = \"\"\"\\\n        Called just after a worker exited on SIGINT or SIGQUIT.\n\n        The callable needs to accept one instance variable for the initialized\n        Worker.\n        \"\"\"\n\n\nclass WorkerAbort(Setting):\n    name = \"worker_abort\"\n    section = \"Server Hooks\"\n    validator = validate_callable(1)\n    type = callable\n\n    def worker_abort(worker):\n        pass\n\n    default = staticmethod(worker_abort)\n    desc = \"\"\"\\\n        Called when a worker received the SIGABRT signal.\n\n        This call generally happens on timeout.\n\n        The callable needs to accept one instance variable for the initialized\n        Worker.\n        \"\"\"\n\n\nclass PreExec(Setting):\n    name = \"pre_exec\"\n    section = \"Server Hooks\"\n    validator = validate_callable(1)\n    type = callable\n\n    def pre_exec(server):\n        pass\n    default = staticmethod(pre_exec)\n    desc = \"\"\"\\\n        Called just before a new master process is forked.\n\n        The callable needs to accept a single instance variable for the Arbiter.\n        \"\"\"\n\n\nclass PreRequest(Setting):\n    name = \"pre_request\"\n    section = \"Server Hooks\"\n    validator = validate_callable(2)\n    type = callable\n\n    def pre_request(worker, req):\n        worker.log.debug(\"%s %s\", req.method, req.path)\n    default = staticmethod(pre_request)\n    desc = \"\"\"\\\n        Called just before a worker processes the request.\n\n        The callable needs to accept two instance variables for the Worker and\n        the Request.\n        \"\"\"\n\n\nclass PostRequest(Setting):\n    name = \"post_request\"\n    section = \"Server Hooks\"\n    validator = validate_post_request\n    type = callable\n\n    def post_request(worker, req, environ, resp):\n        pass\n    default = staticmethod(post_request)\n    desc = \"\"\"\\\n        Called after a worker processes the request.\n\n        The callable needs to accept two instance variables for the Worker and\n        the Request. If a third parameter is defined it will be passed the\n        environment. If a fourth parameter is defined it will be passed the Response.\n        \"\"\"\n\n\nclass ChildExit(Setting):\n    name = \"child_exit\"\n    section = \"Server Hooks\"\n    validator = validate_callable(2)\n    type = callable\n\n    def child_exit(server, worker):\n        pass\n    default = staticmethod(child_exit)\n    desc = \"\"\"\\\n        Called just after a worker has been exited, in the master process.\n\n        The callable needs to accept two instance variables for the Arbiter and\n        the just-exited Worker.\n\n        .. versionadded:: 19.7\n        \"\"\"\n\n\nclass WorkerExit(Setting):\n    name = \"worker_exit\"\n    section = \"Server Hooks\"\n    validator = validate_callable(2)\n    type = callable\n\n    def worker_exit(server, worker):\n        pass\n    default = staticmethod(worker_exit)\n    desc = \"\"\"\\\n        Called just after a worker has been exited, in the worker process.\n\n        The callable needs to accept two instance variables for the Arbiter and\n        the just-exited Worker.\n        \"\"\"\n\n\nclass NumWorkersChanged(Setting):\n    name = \"nworkers_changed\"\n    section = \"Server Hooks\"\n    validator = validate_callable(3)\n    type = callable\n\n    def nworkers_changed(server, new_value, old_value):\n        pass\n    default = staticmethod(nworkers_changed)\n    desc = \"\"\"\\\n        Called just after *num_workers* has been changed.\n\n        The callable needs to accept an instance variable of the Arbiter and\n        two integers of number of workers after and before change.\n\n        If the number of workers is set for the first time, *old_value* would\n        be ``None``.\n        \"\"\"\n\n\nclass OnExit(Setting):\n    name = \"on_exit\"\n    section = \"Server Hooks\"\n    validator = validate_callable(1)\n\n    def on_exit(server):\n        pass\n\n    default = staticmethod(on_exit)\n    desc = \"\"\"\\\n        Called just before exiting Gunicorn.\n\n        The callable needs to accept a single instance variable for the Arbiter.\n        \"\"\"\n\n\nclass NewSSLContext(Setting):\n    name = \"ssl_context\"\n    section = \"Server Hooks\"\n    validator = validate_callable(2)\n    type = callable\n\n    def ssl_context(config, default_ssl_context_factory):\n        return default_ssl_context_factory()\n\n    default = staticmethod(ssl_context)\n    desc = \"\"\"\\\n        Called when SSLContext is needed.\n\n        Allows customizing SSL context.\n\n        The callable needs to accept an instance variable for the Config and\n        a factory function that returns default SSLContext which is initialized\n        with certificates, private key, cert_reqs, and ciphers according to\n        config and can be further customized by the callable.\n        The callable needs to return SSLContext object.\n\n        Following example shows a configuration file that sets the minimum TLS version to 1.3:\n\n        .. code-block:: python\n\n            def ssl_context(conf, default_ssl_context_factory):\n                import ssl\n                context = default_ssl_context_factory()\n                context.minimum_version = ssl.TLSVersion.TLSv1_3\n                return context\n\n        .. versionadded:: 21.0\n        \"\"\"\n\n\ndef validate_proxy_protocol(val):\n    \"\"\"Validate proxy_protocol setting.\n\n    Accepts: off, false, v1, v2, auto, true\n    Returns normalized value: off, v1, v2, or auto\n    \"\"\"\n    if val is None:\n        return \"off\"\n    if isinstance(val, bool):\n        return \"auto\" if val else \"off\"\n    if not isinstance(val, str):\n        raise TypeError(\"proxy_protocol must be string or bool\")\n\n    val = val.lower().strip()\n    mapping = {\n        \"false\": \"off\", \"off\": \"off\", \"0\": \"off\", \"none\": \"off\",\n        \"true\": \"auto\", \"auto\": \"auto\", \"1\": \"auto\",\n        \"v1\": \"v1\", \"v2\": \"v2\",\n    }\n    if val not in mapping:\n        raise ValueError(\"proxy_protocol must be: off, v1, v2, or auto\")\n    return mapping[val]\n\n\nclass ProxyProtocol(Setting):\n    name = \"proxy_protocol\"\n    section = \"Server Mechanics\"\n    cli = [\"--proxy-protocol\"]\n    meta = \"MODE\"\n    validator = validate_proxy_protocol\n    default = \"off\"\n    nargs = \"?\"\n    const = \"auto\"\n    desc = \"\"\"\\\n        Enable PROXY protocol support.\n\n        Allow using HTTP and PROXY protocol together. It may be useful for work\n        with stunnel as HTTPS frontend and Gunicorn as HTTP server, or with\n        HAProxy.\n\n        Accepted values:\n\n        * ``off`` - Disabled (default)\n        * ``v1`` - PROXY protocol v1 only (text format)\n        * ``v2`` - PROXY protocol v2 only (binary format)\n        * ``auto`` - Auto-detect v1 or v2\n\n        Using ``--proxy-protocol`` without a value is equivalent to ``auto``.\n\n        PROXY protocol v1: http://haproxy.1wt.eu/download/1.5/doc/proxy-protocol.txt\n        PROXY protocol v2: https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt\n\n        Example for stunnel config::\n\n            [https]\n            protocol = proxy\n            accept  = 443\n            connect = 80\n            cert = /etc/ssl/certs/stunnel.pem\n            key = /etc/ssl/certs/stunnel.key\n\n        .. versionchanged:: 24.1.0\n           Extended to support version selection (v1, v2, auto).\n        \"\"\"\n\n\nclass ProxyAllowFrom(Setting):\n    name = \"proxy_allow_ips\"\n    section = \"Server Mechanics\"\n    cli = [\"--proxy-allow-from\"]\n    validator = validate_string_to_addr_list\n    default = \"127.0.0.1,::1\"\n    desc = \"\"\"\\\n        Front-end's IP addresses or networks from which allowed accept\n        proxy requests (comma separated).\n\n        Supports both individual IP addresses (e.g., ``192.168.1.1``) and\n        CIDR networks (e.g., ``192.168.0.0/16``).\n\n        Set to ``*`` to disable checking of front-end IPs. This is useful for setups\n        where you don't know in advance the IP address of front-end, but\n        instead have ensured via other means that only your\n        authorized front-ends can access Gunicorn.\n\n        .. note::\n\n            This option does not affect UNIX socket connections. Connections not associated with\n            an IP address are treated as allowed, unconditionally.\n        \"\"\"\n\n\nclass Protocol(Setting):\n    name = \"protocol\"\n    section = \"Server Mechanics\"\n    cli = [\"--protocol\"]\n    meta = \"STRING\"\n    validator = validate_string\n    default = \"http\"\n    desc = \"\"\"\\\n        The protocol for incoming connections.\n\n        * ``http`` - Standard HTTP/1.x (default)\n        * ``uwsgi`` - uWSGI binary protocol (for nginx uwsgi_pass)\n\n        When using the uWSGI protocol, Gunicorn can receive requests from\n        nginx using the uwsgi_pass directive::\n\n            upstream gunicorn {\n                server 127.0.0.1:8000;\n            }\n            location / {\n                uwsgi_pass gunicorn;\n                include uwsgi_params;\n            }\n        \"\"\"\n\n\nclass UWSGIAllowFrom(Setting):\n    name = \"uwsgi_allow_ips\"\n    section = \"Server Mechanics\"\n    cli = [\"--uwsgi-allow-from\"]\n    validator = validate_string_to_addr_list\n    default = \"127.0.0.1,::1\"\n    desc = \"\"\"\\\n        IPs allowed to send uWSGI protocol requests (comma separated).\n\n        Set to ``*`` to allow all IPs. This is useful for setups where you\n        don't know in advance the IP address of front-end, but instead have\n        ensured via other means that only your authorized front-ends can\n        access Gunicorn.\n\n        .. note::\n\n            This option does not affect UNIX socket connections. Connections not associated with\n            an IP address are treated as allowed, unconditionally.\n        \"\"\"\n\n\nclass KeyFile(Setting):\n    name = \"keyfile\"\n    section = \"SSL\"\n    cli = [\"--keyfile\"]\n    meta = \"FILE\"\n    validator = validate_string\n    default = None\n    desc = \"\"\"\\\n    SSL key file\n    \"\"\"\n\n\nclass CertFile(Setting):\n    name = \"certfile\"\n    section = \"SSL\"\n    cli = [\"--certfile\"]\n    meta = \"FILE\"\n    validator = validate_string\n    default = None\n    desc = \"\"\"\\\n    SSL certificate file\n    \"\"\"\n\n\nclass SSLVersion(Setting):\n    name = \"ssl_version\"\n    section = \"SSL\"\n    cli = [\"--ssl-version\"]\n    validator = validate_ssl_version\n\n    if hasattr(ssl, \"PROTOCOL_TLS\"):\n        default = ssl.PROTOCOL_TLS\n    else:\n        default = ssl.PROTOCOL_SSLv23\n\n    default = ssl.PROTOCOL_SSLv23\n    desc = \"\"\"\\\n    SSL version to use (see stdlib ssl module's).\n\n    .. deprecated:: 21.0\n       The option is deprecated and it is currently ignored. Use :ref:`ssl-context` instead.\n\n    ============= ============\n    --ssl-version Description\n    ============= ============\n    SSLv3         SSLv3 is not-secure and is strongly discouraged.\n    SSLv23        Alias for TLS. Deprecated in Python 3.6, use TLS.\n    TLS           Negotiate highest possible version between client/server.\n                  Can yield SSL. (Python 3.6+)\n    TLSv1         TLS 1.0\n    TLSv1_1       TLS 1.1 (Python 3.4+)\n    TLSv1_2       TLS 1.2 (Python 3.4+)\n    TLS_SERVER    Auto-negotiate the highest protocol version like TLS,\n                  but only support server-side SSLSocket connections.\n                  (Python 3.6+)\n    ============= ============\n\n    .. versionchanged:: 19.7\n       The default value has been changed from ``ssl.PROTOCOL_TLSv1`` to\n       ``ssl.PROTOCOL_SSLv23``.\n    .. versionchanged:: 20.0\n       This setting now accepts string names based on ``ssl.PROTOCOL_``\n       constants.\n    .. versionchanged:: 20.0.1\n       The default value has been changed from ``ssl.PROTOCOL_SSLv23`` to\n       ``ssl.PROTOCOL_TLS`` when Python >= 3.6 .\n    \"\"\"\n\n\nclass CertReqs(Setting):\n    name = \"cert_reqs\"\n    section = \"SSL\"\n    cli = [\"--cert-reqs\"]\n    validator = validate_pos_int\n    default = ssl.CERT_NONE\n    desc = \"\"\"\\\n    Whether client certificate is required (see stdlib ssl module's)\n\n    ===========  ===========================\n    --cert-reqs      Description\n    ===========  ===========================\n    `0`          no client verification\n    `1`          ssl.CERT_OPTIONAL\n    `2`          ssl.CERT_REQUIRED\n    ===========  ===========================\n    \"\"\"\n\n\nclass CACerts(Setting):\n    name = \"ca_certs\"\n    section = \"SSL\"\n    cli = [\"--ca-certs\"]\n    meta = \"FILE\"\n    validator = validate_string\n    default = None\n    desc = \"\"\"\\\n    CA certificates file\n    \"\"\"\n\n\nclass SuppressRaggedEOFs(Setting):\n    name = \"suppress_ragged_eofs\"\n    section = \"SSL\"\n    cli = [\"--suppress-ragged-eofs\"]\n    action = \"store_true\"\n    default = True\n    validator = validate_bool\n    desc = \"\"\"\\\n    Suppress ragged EOFs (see stdlib ssl module's)\n    \"\"\"\n\n\nclass DoHandshakeOnConnect(Setting):\n    name = \"do_handshake_on_connect\"\n    section = \"SSL\"\n    cli = [\"--do-handshake-on-connect\"]\n    validator = validate_bool\n    action = \"store_true\"\n    default = False\n    desc = \"\"\"\\\n    Whether to perform SSL handshake on socket connect (see stdlib ssl module's)\n    \"\"\"\n\n\nclass Ciphers(Setting):\n    name = \"ciphers\"\n    section = \"SSL\"\n    cli = [\"--ciphers\"]\n    validator = validate_string\n    default = None\n    desc = \"\"\"\\\n    SSL Cipher suite to use, in the format of an OpenSSL cipher list.\n\n    By default we use the default cipher list from Python's ``ssl`` module,\n    which contains ciphers considered strong at the time of each Python\n    release.\n\n    As a recommended alternative, the Open Web App Security Project (OWASP)\n    offers `a vetted set of strong cipher strings rated A+ to C-\n    <https://www.owasp.org/index.php/TLS_Cipher_String_Cheat_Sheet>`_.\n    OWASP provides details on user-agent compatibility at each security level.\n\n    See the `OpenSSL Cipher List Format Documentation\n    <https://www.openssl.org/docs/manmaster/man1/ciphers.html#CIPHER-LIST-FORMAT>`_\n    for details on the format of an OpenSSL cipher list.\n    \"\"\"\n\n\n# HTTP/2 Protocol Settings\n\n# Valid protocol identifiers\nVALID_HTTP_PROTOCOLS = frozenset([\"h1\", \"h2\", \"h3\"])\n# Map protocol identifiers to ALPN protocol names\nALPN_PROTOCOL_MAP = {\n    \"h1\": \"http/1.1\",\n    \"h2\": \"h2\",\n    \"h3\": \"h3\",  # Future: HTTP/3 over QUIC\n}\n\n\ndef validate_http_protocols(val):\n    \"\"\"Validate http_protocols setting.\n\n    Accepts comma-separated list of protocol identifiers.\n    Valid values: h1 (HTTP/1.1), h2 (HTTP/2), h3 (HTTP/3 - future)\n    Order indicates preference (first = most preferred).\n    \"\"\"\n    if val is None:\n        return [\"h1\"]\n    if not isinstance(val, str):\n        raise TypeError(\"http_protocols must be a string\")\n\n    val = val.strip()\n    if not val:\n        return [\"h1\"]\n\n    protocols = [p.strip().lower() for p in val.split(\",\") if p.strip()]\n    if not protocols:\n        return [\"h1\"]\n\n    # Validate each protocol\n    for proto in protocols:\n        if proto not in VALID_HTTP_PROTOCOLS:\n            raise ValueError(\n                f\"Invalid protocol '{proto}'. \"\n                f\"Valid protocols: {', '.join(sorted(VALID_HTTP_PROTOCOLS))}\"\n            )\n\n    # Check for duplicates\n    if len(protocols) != len(set(protocols)):\n        raise ValueError(\"Duplicate protocols specified\")\n\n    return protocols\n\n\nclass HTTPProtocols(Setting):\n    name = \"http_protocols\"\n    section = \"HTTP/2\"\n    cli = [\"--http-protocols\"]\n    meta = \"STRING\"\n    validator = validate_http_protocols\n    default = \"h1\"\n    desc = \"\"\"\\\n        HTTP protocol versions to support (comma-separated, order = preference).\n\n        Valid protocols:\n\n        * ``h1`` - HTTP/1.1 (default)\n        * ``h2`` - HTTP/2 (requires TLS with ALPN)\n        * ``h3`` - HTTP/3 (future, not yet implemented)\n\n        Examples::\n\n            # HTTP/1.1 only (default, backward compatible)\n            --http-protocols=h1\n\n            # Prefer HTTP/2, fallback to HTTP/1.1\n            --http-protocols=h2,h1\n\n            # HTTP/2 only (reject HTTP/1.1 clients)\n            --http-protocols=h2\n\n        HTTP/2 requires:\n\n        * TLS (--certfile and --keyfile)\n        * The h2 library: ``pip install gunicorn[http2]``\n        * ALPN-capable TLS client\n\n        .. note::\n           HTTP/2 cleartext (h2c) is not supported due to security concerns\n           and lack of browser support.\n\n        .. versionadded:: 25.0.0\n        \"\"\"\n\n\nclass HTTP2MaxConcurrentStreams(Setting):\n    name = \"http2_max_concurrent_streams\"\n    section = \"HTTP/2\"\n    cli = [\"--http2-max-concurrent-streams\"]\n    meta = \"INT\"\n    validator = validate_pos_int\n    type = int\n    default = 100\n    desc = \"\"\"\\\n        Maximum number of concurrent HTTP/2 streams per connection.\n\n        This limits how many requests can be processed simultaneously on a\n        single HTTP/2 connection. Higher values allow more parallelism but\n        use more memory.\n\n        Default is 100, which matches common server configurations.\n        The HTTP/2 specification allows up to 2^31-1.\n\n        .. versionadded:: 25.0.0\n        \"\"\"\n\n\nclass HTTP2InitialWindowSize(Setting):\n    name = \"http2_initial_window_size\"\n    section = \"HTTP/2\"\n    cli = [\"--http2-initial-window-size\"]\n    meta = \"INT\"\n    validator = validate_pos_int\n    type = int\n    default = 65535\n    desc = \"\"\"\\\n        Initial HTTP/2 flow control window size in bytes.\n\n        This controls how much data can be in-flight before the receiver\n        sends WINDOW_UPDATE frames. Larger values can improve throughput\n        for large transfers but use more memory.\n\n        Default is 65535 (64KB - 1), the HTTP/2 specification default.\n        Maximum is 2^31-1 (2147483647).\n\n        .. versionadded:: 25.0.0\n        \"\"\"\n\n\nclass HTTP2MaxFrameSize(Setting):\n    name = \"http2_max_frame_size\"\n    section = \"HTTP/2\"\n    cli = [\"--http2-max-frame-size\"]\n    meta = \"INT\"\n    validator = validate_http2_frame_size\n    type = int\n    default = 16384\n    desc = \"\"\"\\\n        Maximum HTTP/2 frame payload size in bytes.\n\n        This is the largest frame payload the server will accept.\n        Larger frames reduce framing overhead but may increase latency\n        for small messages.\n\n        Default is 16384 (16KB), the HTTP/2 specification minimum.\n        Range is 16384 to 16777215 (16MB - 1).\n\n        .. versionadded:: 25.0.0\n        \"\"\"\n\n\nclass HTTP2MaxHeaderListSize(Setting):\n    name = \"http2_max_header_list_size\"\n    section = \"HTTP/2\"\n    cli = [\"--http2-max-header-list-size\"]\n    meta = \"INT\"\n    validator = validate_pos_int\n    type = int\n    default = 65536\n    desc = \"\"\"\\\n        Maximum size of HTTP/2 header list in bytes (HPACK protection).\n\n        This limits the total size of headers after HPACK decompression.\n        Protects against compression bombs and excessive memory use.\n\n        Default is 65536 (64KB). Set to 0 for unlimited (not recommended).\n\n        .. versionadded:: 25.0.0\n        \"\"\"\n\n\nclass PasteGlobalConf(Setting):\n    name = \"raw_paste_global_conf\"\n    action = \"append\"\n    section = \"Server Mechanics\"\n    cli = [\"--paste-global\"]\n    meta = \"CONF\"\n    validator = validate_list_string\n    default = []\n\n    desc = \"\"\"\\\n        Set a PasteDeploy global config variable in ``key=value`` form.\n\n        The option can be specified multiple times.\n\n        The variables are passed to the PasteDeploy entrypoint. Example::\n\n            $ gunicorn -b 127.0.0.1:8000 --paste development.ini --paste-global FOO=1 --paste-global BAR=2\n\n        .. versionadded:: 19.7\n        \"\"\"\n\n\nclass PermitObsoleteFolding(Setting):\n    name = \"permit_obsolete_folding\"\n    section = \"Server Mechanics\"\n    cli = [\"--permit-obsolete-folding\"]\n    validator = validate_bool\n    action = \"store_true\"\n    default = False\n    desc = \"\"\"\\\n        Permit requests employing obsolete HTTP line folding mechanism\n\n        The folding mechanism was deprecated by rfc7230 Section 3.2.4 and will not be\n         employed in HTTP request headers from standards-compliant HTTP clients.\n\n        This option is provided to diagnose backwards-incompatible changes.\n        Use with care and only if necessary. Temporary; the precise effect of this option may\n        change in a future version, or it may be removed altogether.\n\n        .. versionadded:: 23.0.0\n        \"\"\"\n\n\nclass StripHeaderSpaces(Setting):\n    name = \"strip_header_spaces\"\n    section = \"Server Mechanics\"\n    cli = [\"--strip-header-spaces\"]\n    validator = validate_bool\n    action = \"store_true\"\n    default = False\n    desc = \"\"\"\\\n        Strip spaces present between the header name and the the ``:``.\n\n        This is known to induce vulnerabilities and is not compliant with the HTTP/1.1 standard.\n        See https://portswigger.net/research/http-desync-attacks-request-smuggling-reborn.\n\n        Use with care and only if necessary. Deprecated; scheduled for removal in 25.0.0\n\n        .. versionadded:: 20.0.1\n        \"\"\"\n\n\nclass PermitUnconventionalHTTPMethod(Setting):\n    name = \"permit_unconventional_http_method\"\n    section = \"Server Mechanics\"\n    cli = [\"--permit-unconventional-http-method\"]\n    validator = validate_bool\n    action = \"store_true\"\n    default = False\n    desc = \"\"\"\\\n        Permit HTTP methods not matching conventions, such as IANA registration guidelines\n\n        This permits request methods of length less than 3 or more than 20,\n        methods with lowercase characters or methods containing the # character.\n        HTTP methods are case sensitive by definition, and merely uppercase by convention.\n\n        If unset, Gunicorn will apply nonstandard restrictions and cause 400 response status\n        in cases where otherwise 501 status is expected. While this option does modify that\n        behaviour, it should not be depended upon to guarantee standards-compliant behaviour.\n        Rather, it is provided temporarily, to assist in diagnosing backwards-incompatible\n        changes around the incomplete application of those restrictions.\n\n        Use with care and only if necessary. Temporary; scheduled for removal in 24.0.0\n\n        .. versionadded:: 22.0.0\n        \"\"\"\n\n\nclass PermitUnconventionalHTTPVersion(Setting):\n    name = \"permit_unconventional_http_version\"\n    section = \"Server Mechanics\"\n    cli = [\"--permit-unconventional-http-version\"]\n    validator = validate_bool\n    action = \"store_true\"\n    default = False\n    desc = \"\"\"\\\n        Permit HTTP version not matching conventions of 2023\n\n        This disables the refusal of likely malformed request lines.\n        It is unusual to specify HTTP 1 versions other than 1.0 and 1.1.\n\n        This option is provided to diagnose backwards-incompatible changes.\n        Use with care and only if necessary. Temporary; the precise effect of this option may\n        change in a future version, or it may be removed altogether.\n\n        .. versionadded:: 22.0.0\n        \"\"\"\n\n\nclass CasefoldHTTPMethod(Setting):\n    name = \"casefold_http_method\"\n    section = \"Server Mechanics\"\n    cli = [\"--casefold-http-method\"]\n    validator = validate_bool\n    action = \"store_true\"\n    default = False\n    desc = \"\"\"\\\n         Transform received HTTP methods to uppercase\n\n         HTTP methods are case sensitive by definition, and merely uppercase by convention.\n\n         This option is provided because previous versions of gunicorn defaulted to this behaviour.\n\n         Use with care and only if necessary. Deprecated; scheduled for removal in 24.0.0\n\n         .. versionadded:: 22.0.0\n         \"\"\"\n\n\ndef validate_header_map_behaviour(val):\n    # FIXME: refactor all of this subclassing stdlib argparse\n\n    if val is None:\n        return\n\n    if not isinstance(val, str):\n        raise TypeError(\"Invalid type for casting: %s\" % val)\n    if val.lower().strip() == \"drop\":\n        return \"drop\"\n    elif val.lower().strip() == \"refuse\":\n        return \"refuse\"\n    elif val.lower().strip() == \"dangerous\":\n        return \"dangerous\"\n    else:\n        raise ValueError(\"Invalid header map behaviour: %s\" % val)\n\n\nclass ForwarderHeaders(Setting):\n    name = \"forwarder_headers\"\n    section = \"Server Mechanics\"\n    cli = [\"--forwarder-headers\"]\n    validator = validate_string_to_list\n    default = \"SCRIPT_NAME,PATH_INFO\"\n    desc = \"\"\"\\\n\n        A list containing upper-case header field names that the front-end proxy\n        (see :ref:`forwarded-allow-ips`) sets, to be used in WSGI environment.\n\n        This option has no effect for headers not present in the request.\n\n        This option can be used to transfer ``SCRIPT_NAME``, ``PATH_INFO``\n        and ``REMOTE_USER``.\n\n        It is important that your front-end proxy configuration ensures that\n        the headers defined here can not be passed directly from the client.\n        \"\"\"\n\n\nclass HeaderMap(Setting):\n    name = \"header_map\"\n    section = \"Server Mechanics\"\n    cli = [\"--header-map\"]\n    validator = validate_header_map_behaviour\n    default = \"drop\"\n    desc = \"\"\"\\\n        Configure how header field names are mapped into environ\n\n        Headers containing underscores are permitted by RFC9110,\n        but gunicorn joining headers of different names into\n        the same environment variable will dangerously confuse applications as to which is which.\n\n        The safe default ``drop`` is to silently drop headers that cannot be unambiguously mapped.\n        The value ``refuse`` will return an error if a request contains *any* such header.\n        The value ``dangerous`` matches the previous, not advisable, behaviour of mapping different\n        header field names into the same environ name.\n\n        If the source is permitted as explained in :ref:`forwarded-allow-ips`, *and* the header name is\n        present in :ref:`forwarder-headers`, the header is mapped into environment regardless of\n        the state of this setting.\n\n        Use with care and only if necessary and after considering if your problem could\n        instead be solved by specifically renaming or rewriting only the intended headers\n        on a proxy in front of Gunicorn.\n\n        .. versionadded:: 22.0.0\n        \"\"\"\n\n\ndef validate_asgi_loop(val):\n    if val is None:\n        return \"auto\"\n    if not isinstance(val, str):\n        raise TypeError(\"Invalid type for casting: %s\" % val)\n    val = val.lower().strip()\n    if val not in (\"auto\", \"asyncio\", \"uvloop\"):\n        raise ValueError(\"Invalid ASGI loop: %s\" % val)\n    return val\n\n\ndef validate_asgi_lifespan(val):\n    if val is None:\n        return \"auto\"\n    if not isinstance(val, str):\n        raise TypeError(\"Invalid type for casting: %s\" % val)\n    val = val.lower().strip()\n    if val not in (\"auto\", \"on\", \"off\"):\n        raise ValueError(\"Invalid ASGI lifespan: %s\" % val)\n    return val\n\n\nclass ASGILoop(Setting):\n    name = \"asgi_loop\"\n    section = \"Worker Processes\"\n    cli = [\"--asgi-loop\"]\n    meta = \"STRING\"\n    validator = validate_asgi_loop\n    default = \"auto\"\n    desc = \"\"\"\\\n        Event loop implementation for ASGI workers.\n\n        - auto: Use uvloop if available, otherwise asyncio\n        - asyncio: Use Python's built-in asyncio event loop\n        - uvloop: Use uvloop (must be installed separately)\n\n        This setting only affects the ``asgi`` worker type.\n\n        uvloop typically provides better performance but requires\n        installing the uvloop package.\n\n        .. versionadded:: 24.0.0\n        \"\"\"\n\n\nclass ASGILifespan(Setting):\n    name = \"asgi_lifespan\"\n    section = \"Worker Processes\"\n    cli = [\"--asgi-lifespan\"]\n    meta = \"STRING\"\n    validator = validate_asgi_lifespan\n    default = \"auto\"\n    desc = \"\"\"\\\n        Control ASGI lifespan protocol handling.\n\n        - auto: Detect if app supports lifespan, enable if so\n        - on: Always run lifespan protocol (fail if unsupported)\n        - off: Never run lifespan protocol\n\n        The lifespan protocol allows ASGI applications to run code at\n        startup and shutdown. This is essential for frameworks like\n        FastAPI that need to initialize database connections, caches,\n        or other resources.\n\n        This setting only affects the ``asgi`` worker type.\n\n        .. versionadded:: 24.0.0\n        \"\"\"\n\n\nclass ASGIDisconnectGracePeriod(Setting):\n    name = \"asgi_disconnect_grace_period\"\n    section = \"Worker Processes\"\n    cli = [\"--asgi-disconnect-grace-period\"]\n    meta = \"INT\"\n    validator = validate_pos_int\n    type = int\n    default = 3\n    desc = \"\"\"\\\n        Grace period (seconds) for ASGI apps to handle client disconnects.\n\n        When a client disconnects, the ASGI app receives an http.disconnect\n        message and has this many seconds to clean up resources (like database\n        connections) before the request task is cancelled.\n\n        Set to 0 to cancel immediately (not recommended for apps with async\n        database connections). Apps with long-running database operations may\n        need to increase this value.\n\n        This setting only affects the ``asgi`` worker type.\n\n        .. versionadded:: 25.0.0\n        \"\"\"\n\n\nclass RootPath(Setting):\n    name = \"root_path\"\n    section = \"Server Mechanics\"\n    cli = [\"--root-path\"]\n    meta = \"STRING\"\n    validator = validate_string\n    default = \"\"\n    desc = \"\"\"\\\n        The root path for ASGI applications.\n\n        This is used to set the ``root_path`` in the ASGI scope, which\n        allows applications to know their mount point when behind a\n        reverse proxy.\n\n        For example, if your application is mounted at ``/api``, set\n        this to ``/api``.\n\n        .. versionadded:: 24.0.0\n        \"\"\"\n\n\n# =============================================================================\n# Dirty Arbiters - Separate process pool for long-running operations\n# =============================================================================\n\nclass DirtyApps(Setting):\n    name = \"dirty_apps\"\n    section = \"Dirty Arbiters\"\n    cli = [\"--dirty-app\"]\n    action = \"append\"\n    meta = \"STRING\"\n    validator = validate_list_string\n    default = []\n    desc = \"\"\"\\\n        Dirty applications to load in the dirty worker pool.\n\n        A list of application paths in one of these formats:\n\n        - ``$(MODULE_NAME):$(CLASS_NAME)`` - all workers load this app\n        - ``$(MODULE_NAME):$(CLASS_NAME):$(N)`` - only N workers load this app\n\n        Each dirty app must be a class that inherits from ``DirtyApp`` base class\n        and implements the ``init()``, ``__call__()``, and ``close()`` methods.\n\n        Example::\n\n            dirty_apps = [\n                \"myapp.ml:MLApp\",           # All workers load this\n                \"myapp.images:ImageApp\",    # All workers load this\n                \"myapp.heavy:HugeModel:2\",  # Only 2 workers load this\n            ]\n\n        The per-app worker limit is useful for memory-intensive applications\n        like large ML models. Instead of all 8 workers loading a 10GB model\n        (80GB total), you can limit it to 2 workers (20GB total).\n\n        Alternatively, you can set the ``workers`` class attribute on your\n        DirtyApp subclass::\n\n            class HugeModelApp(DirtyApp):\n                workers = 2  # Only 2 workers load this app\n\n                def init(self):\n                    self.model = load_10gb_model()\n\n        Note: The config format (``module:Class:N``) takes precedence over\n        the class attribute if both are specified.\n\n        Dirty apps are loaded once when the dirty worker starts and persist\n        in memory for the lifetime of the worker. This is ideal for loading\n        ML models, database connection pools, or other stateful resources\n        that are expensive to initialize.\n\n        .. versionadded:: 25.0.0\n\n        .. versionchanged:: 25.1.0\n           Added per-app worker allocation via ``:N`` format suffix.\n        \"\"\"\n\n\nclass DirtyWorkers(Setting):\n    name = \"dirty_workers\"\n    section = \"Dirty Arbiters\"\n    cli = [\"--dirty-workers\"]\n    meta = \"INT\"\n    validator = validate_pos_int\n    type = int\n    default = 0\n    desc = \"\"\"\\\n        The number of dirty worker processes.\n\n        A positive integer. Set to 0 (default) to disable the dirty arbiter.\n        When set to a positive value, a dirty arbiter process will be spawned\n        to manage the dirty worker pool.\n\n        Dirty workers are separate from HTTP workers and are designed for\n        long-running, blocking operations like ML model inference or heavy\n        computation.\n\n        .. versionadded:: 25.0.0\n        \"\"\"\n\n\nclass DirtyTimeout(Setting):\n    name = \"dirty_timeout\"\n    section = \"Dirty Arbiters\"\n    cli = [\"--dirty-timeout\"]\n    meta = \"INT\"\n    validator = validate_pos_int\n    type = int\n    default = 300\n    desc = \"\"\"\\\n        Timeout for dirty task execution in seconds.\n\n        Workers silent for more than this many seconds are considered stuck\n        and will be killed. Set to a high value for operations like model\n        loading that may take a long time.\n\n        Value is a positive number. Setting it to 0 disables timeout checking.\n\n        .. versionadded:: 25.0.0\n        \"\"\"\n\n\nclass DirtyThreads(Setting):\n    name = \"dirty_threads\"\n    section = \"Dirty Arbiters\"\n    cli = [\"--dirty-threads\"]\n    meta = \"INT\"\n    validator = validate_pos_int\n    type = int\n    default = 1\n    desc = \"\"\"\\\n        The number of threads per dirty worker.\n\n        Each dirty worker can use threads to handle concurrent operations\n        within the same process, useful for async-safe applications.\n\n        .. versionadded:: 25.0.0\n        \"\"\"\n\n\nclass DirtyGracefulTimeout(Setting):\n    name = \"dirty_graceful_timeout\"\n    section = \"Dirty Arbiters\"\n    cli = [\"--dirty-graceful-timeout\"]\n    meta = \"INT\"\n    validator = validate_pos_int\n    type = int\n    default = 30\n    desc = \"\"\"\\\n        Timeout for graceful dirty worker shutdown in seconds.\n\n        After receiving a shutdown signal, dirty workers have this much time\n        to finish their current tasks. Workers still alive after the timeout\n        are force killed.\n\n        .. versionadded:: 25.0.0\n        \"\"\"\n\n\n# =============================================================================\n# Dirty Arbiter Hooks\n# =============================================================================\n\nclass OnDirtyStarting(Setting):\n    name = \"on_dirty_starting\"\n    section = \"Dirty Arbiter Hooks\"\n    validator = validate_callable(1)\n    type = callable\n\n    def on_dirty_starting(arbiter):\n        pass\n    default = staticmethod(on_dirty_starting)\n    desc = \"\"\"\\\n        Called just before the dirty arbiter process is initialized.\n\n        The callable needs to accept a single instance variable for the\n        DirtyArbiter.\n\n        .. versionadded:: 25.0.0\n        \"\"\"\n\n\nclass DirtyPostFork(Setting):\n    name = \"dirty_post_fork\"\n    section = \"Dirty Arbiter Hooks\"\n    validator = validate_callable(2)\n    type = callable\n\n    def dirty_post_fork(arbiter, worker):\n        pass\n    default = staticmethod(dirty_post_fork)\n    desc = \"\"\"\\\n        Called just after a dirty worker has been forked.\n\n        The callable needs to accept two instance variables for the\n        DirtyArbiter and new DirtyWorker.\n\n        .. versionadded:: 25.0.0\n        \"\"\"\n\n\nclass DirtyWorkerInit(Setting):\n    name = \"dirty_worker_init\"\n    section = \"Dirty Arbiter Hooks\"\n    validator = validate_callable(1)\n    type = callable\n\n    def dirty_worker_init(worker):\n        pass\n    default = staticmethod(dirty_worker_init)\n    desc = \"\"\"\\\n        Called just after a dirty worker has initialized all applications.\n\n        The callable needs to accept one instance variable for the\n        DirtyWorker.\n\n        .. versionadded:: 25.0.0\n        \"\"\"\n\n\nclass DirtyWorkerExit(Setting):\n    name = \"dirty_worker_exit\"\n    section = \"Dirty Arbiter Hooks\"\n    validator = validate_callable(2)\n    type = callable\n\n    def dirty_worker_exit(arbiter, worker):\n        pass\n    default = staticmethod(dirty_worker_exit)\n    desc = \"\"\"\\\n        Called when a dirty worker has exited.\n\n        The callable needs to accept two instance variables for the\n        DirtyArbiter and the exiting DirtyWorker.\n\n        .. versionadded:: 25.0.0\n        \"\"\"\n\n\n# Control Socket Settings\n\nclass ControlSocket(Setting):\n    name = \"control_socket\"\n    section = \"Control\"\n    cli = [\"--control-socket\"]\n    meta = \"PATH\"\n    validator = validate_string\n    default = \"/run/gunicorn.ctl\"\n    desc = \"\"\"\\\n        Unix socket path for control interface.\n\n        The control socket allows runtime management of Gunicorn via the\n        ``gunicornc`` command-line tool. Commands include viewing worker\n        status, adjusting worker count, and graceful reload/shutdown.\n\n        By default, creates ``/run/gunicorn.ctl`` (requires write access to\n        ``/run``). For user-level deployments, specify a different path such\n        as ``/tmp/gunicorn.ctl`` or ``~/.gunicorn.ctl``.\n\n        Use ``--no-control-socket`` to disable.\n\n        .. versionadded:: 25.1.0\n        \"\"\"\n\n\nclass ControlSocketMode(Setting):\n    name = \"control_socket_mode\"\n    section = \"Control\"\n    cli = [\"--control-socket-mode\"]\n    meta = \"INT\"\n    validator = validate_pos_int\n    type = auto_int\n    default = 0o600\n    desc = \"\"\"\\\n        Permission mode for control socket.\n\n        Restricts who can connect to the control socket. Default ``0600``\n        allows only the socket owner. Set to ``0660`` to allow group access.\n\n        .. versionadded:: 25.1.0\n        \"\"\"\n\n\nclass ControlSocketDisable(Setting):\n    name = \"control_socket_disable\"\n    section = \"Control\"\n    cli = [\"--no-control-socket\"]\n    validator = validate_bool\n    action = \"store_true\"\n    default = False\n    desc = \"\"\"\\\n        Disable control socket.\n\n        When set, no control socket is created and ``gunicornc`` cannot\n        connect to this Gunicorn instance.\n\n        .. versionadded:: 25.1.0\n        \"\"\"\n"
  },
  {
    "path": "gunicorn/ctl/__init__.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nGunicorn Control Interface\n\nProvides a control socket server for runtime management and\na CLI client (gunicornc) for interacting with running Gunicorn instances.\n\"\"\"\n\nfrom gunicorn.ctl.server import ControlSocketServer\nfrom gunicorn.ctl.client import ControlClient\nfrom gunicorn.ctl.protocol import ControlProtocol\n\n__all__ = ['ControlSocketServer', 'ControlClient', 'ControlProtocol']\n"
  },
  {
    "path": "gunicorn/ctl/cli.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\ngunicornc - Gunicorn control interface CLI\n\nInteractive and single-command modes for controlling Gunicorn instances.\n\"\"\"\n\nimport argparse\nimport json\nimport os\nimport sys\n\nfrom gunicorn.ctl.client import ControlClient, ControlClientError, parse_command\n\n\ndef format_workers(data: dict) -> str:\n    \"\"\"Format workers output for display.\"\"\"\n    workers = data.get(\"workers\", [])\n    if not workers:\n        return \"No workers running\"\n\n    lines = []\n    lines.append(f\"{'PID':<10} {'AGE':<6} {'BOOTED':<8} {'LAST_BEAT'}\")\n    lines.append(\"-\" * 40)\n\n    for w in workers:\n        pid = w.get(\"pid\", \"?\")\n        age = w.get(\"age\", \"?\")\n        booted = \"yes\" if w.get(\"booted\") else \"no\"\n        hb = w.get(\"last_heartbeat\")\n        hb_str = f\"{hb}s ago\" if hb is not None else \"n/a\"\n\n        lines.append(f\"{pid:<10} {age:<6} {booted:<8} {hb_str}\")\n\n    lines.append(\"\")\n    lines.append(f\"Total: {data.get('count', len(workers))} workers\")\n\n    return \"\\n\".join(lines)\n\n\ndef format_dirty(data: dict) -> str:\n    \"\"\"Format dirty workers output for display.\"\"\"\n    if not data.get(\"enabled\"):\n        return \"Dirty arbiter not running\"\n\n    lines = []\n    lines.append(f\"Dirty arbiter PID: {data.get('pid')}\")\n    lines.append(\"\")\n\n    workers = data.get(\"workers\", [])\n    if workers:\n        lines.append(\"DIRTY WORKERS:\")\n        lines.append(f\"{'PID':<10} {'AGE':<6} {'APPS':<30} {'LAST_BEAT'}\")\n        lines.append(\"-\" * 60)\n\n        for w in workers:\n            pid = w.get(\"pid\", \"?\")\n            age = w.get(\"age\", \"?\")\n            apps = \", \".join(w.get(\"apps\", []))[:30]\n            hb = w.get(\"last_heartbeat\")\n            hb_str = f\"{hb}s ago\" if hb is not None else \"n/a\"\n\n            lines.append(f\"{pid:<10} {age:<6} {apps:<30} {hb_str}\")\n        lines.append(\"\")\n\n    apps = data.get(\"apps\", [])\n    if apps:\n        lines.append(\"DIRTY APPS:\")\n        lines.append(f\"{'APP':<30} {'WORKERS':<10} {'LIMIT'}\")\n        lines.append(\"-\" * 50)\n\n        for app in apps:\n            path = app.get(\"import_path\", \"?\")[:30]\n            current = app.get(\"current_workers\", 0)\n            limit = app.get(\"worker_count\")\n            limit_str = str(limit) if limit is not None else \"none\"\n\n            lines.append(f\"{path:<30} {current:<10} {limit_str}\")\n\n    return \"\\n\".join(lines)\n\n\ndef format_stats(data: dict) -> str:\n    \"\"\"Format stats output for display.\"\"\"\n    lines = []\n\n    uptime = data.get(\"uptime\")\n    if uptime:\n        hours = int(uptime // 3600)\n        minutes = int((uptime % 3600) // 60)\n        seconds = int(uptime % 60)\n        if hours:\n            uptime_str = f\"{hours}h {minutes}m {seconds}s\"\n        elif minutes:\n            uptime_str = f\"{minutes}m {seconds}s\"\n        else:\n            uptime_str = f\"{seconds}s\"\n    else:\n        uptime_str = \"unknown\"\n\n    lines.append(f\"Uptime:           {uptime_str}\")\n    lines.append(f\"PID:              {data.get('pid', 'unknown')}\")\n    lines.append(f\"Workers current:  {data.get('workers_current', 0)}\")\n    lines.append(f\"Workers target:   {data.get('workers_target', 0)}\")\n    lines.append(f\"Workers spawned:  {data.get('workers_spawned', 0)}\")\n    lines.append(f\"Workers killed:   {data.get('workers_killed', 0)}\")\n    lines.append(f\"Reloads:          {data.get('reloads', 0)}\")\n\n    dirty_pid = data.get(\"dirty_arbiter_pid\")\n    if dirty_pid:\n        lines.append(f\"Dirty arbiter:    {dirty_pid}\")\n\n    return \"\\n\".join(lines)\n\n\ndef format_listeners(data: dict) -> str:\n    \"\"\"Format listeners output for display.\"\"\"\n    listeners = data.get(\"listeners\", [])\n    if not listeners:\n        return \"No listeners bound\"\n\n    lines = []\n    lines.append(f\"{'ADDRESS':<40} {'TYPE':<8} {'FD'}\")\n    lines.append(\"-\" * 55)\n\n    for lnr in listeners:\n        addr = lnr.get(\"address\", \"?\")\n        ltype = lnr.get(\"type\", \"?\")\n        fd = lnr.get(\"fd\", \"?\")\n        lines.append(f\"{addr:<40} {ltype:<8} {fd}\")\n\n    lines.append(\"\")\n    lines.append(f\"Total: {data.get('count', len(listeners))} listeners\")\n\n    return \"\\n\".join(lines)\n\n\ndef format_config(data: dict) -> str:\n    \"\"\"Format config output for display.\"\"\"\n    lines = []\n\n    # Sort keys for consistent output\n    for key in sorted(data.keys()):\n        value = data[key]\n        if isinstance(value, list):\n            value = \", \".join(str(v) for v in value)\n        lines.append(f\"{key}: {value}\")\n\n    return \"\\n\".join(lines)\n\n\ndef format_help(data: dict) -> str:\n    \"\"\"Format help output for display.\"\"\"\n    commands = data.get(\"commands\", {})\n    lines = []\n    lines.append(\"Available commands:\")\n    lines.append(\"\")\n\n    # Find max command length for alignment\n    max_len = max(len(cmd) for cmd in commands.keys()) if commands else 0\n\n    for cmd, desc in sorted(commands.items()):\n        lines.append(f\"  {cmd:<{max_len + 2}} {desc}\")\n\n    return \"\\n\".join(lines)\n\n\ndef format_all(data: dict) -> str:\n    \"\"\"Format show all output for display.\"\"\"\n    lines = []\n\n    # Arbiter\n    arbiter = data.get(\"arbiter\", {})\n    lines.append(\"ARBITER (master)\")\n    lines.append(f\"  PID: {arbiter.get('pid', '?')}\")\n    lines.append(\"\")\n\n    # Web workers\n    web_workers = data.get(\"web_workers\", [])\n    lines.append(f\"WEB WORKERS ({data.get('web_worker_count', 0)})\")\n    if web_workers:\n        lines.append(f\"  {'PID':<10} {'AGE':<6} {'BOOTED':<8} {'LAST_BEAT'}\")\n        lines.append(f\"  {'-' * 38}\")\n        for w in web_workers:\n            pid = w.get(\"pid\", \"?\")\n            age = w.get(\"age\", \"?\")\n            booted = \"yes\" if w.get(\"booted\") else \"no\"\n            hb = w.get(\"last_heartbeat\")\n            hb_str = f\"{hb}s ago\" if hb is not None else \"n/a\"\n            lines.append(f\"  {pid:<10} {age:<6} {booted:<8} {hb_str}\")\n    else:\n        lines.append(\"  (none)\")\n    lines.append(\"\")\n\n    # Dirty arbiter\n    dirty_arbiter = data.get(\"dirty_arbiter\")\n    if dirty_arbiter:\n        lines.append(\"DIRTY ARBITER\")\n        lines.append(f\"  PID: {dirty_arbiter.get('pid', '?')}\")\n        lines.append(\"\")\n\n        # Dirty workers\n        dirty_workers = data.get(\"dirty_workers\", [])\n        lines.append(f\"DIRTY WORKERS ({data.get('dirty_worker_count', 0)})\")\n        if dirty_workers:\n            lines.append(f\"  {'PID':<10} {'AGE':<6} {'APPS'}\")\n            lines.append(f\"  {'-' * 50}\")\n            for w in dirty_workers:\n                pid = w.get(\"pid\", \"?\")\n                age = w.get(\"age\", \"?\")\n                apps = w.get(\"apps\", [])\n                # Show each app on its own line if multiple\n                if apps:\n                    first_app = apps[0].split(\":\")[-1]  # Just the class name\n                    lines.append(f\"  {pid:<10} {age:<6} {first_app}\")\n                    for app in apps[1:]:\n                        app_name = app.split(\":\")[-1]\n                        lines.append(f\"  {'':<10} {'':<6} {app_name}\")\n                else:\n                    lines.append(f\"  {pid:<10} {age:<6} (no apps)\")\n        else:\n            lines.append(\"  (none)\")\n    else:\n        lines.append(\"DIRTY ARBITER\")\n        lines.append(\"  (not running)\")\n\n    return \"\\n\".join(lines)\n\n\ndef format_response(command: str, data: dict) -> str:  # pylint: disable=too-many-return-statements\n    \"\"\"\n    Format response data based on command.\n\n    Args:\n        command: Original command string\n        data: Response data dictionary\n\n    Returns:\n        Formatted string for display\n    \"\"\"\n    cmd_lower = command.lower().strip()\n\n    # Route to specific formatters\n    if cmd_lower == \"show all\":\n        return format_all(data)\n    elif cmd_lower == \"show workers\":\n        return format_workers(data)\n    elif cmd_lower == \"show dirty\":\n        return format_dirty(data)\n    elif cmd_lower == \"show stats\":\n        return format_stats(data)\n    elif cmd_lower == \"show listeners\":\n        return format_listeners(data)\n    elif cmd_lower == \"show config\":\n        return format_config(data)\n    elif cmd_lower == \"help\":\n        return format_help(data)\n    else:\n        # Generic JSON output for other commands\n        if data:\n            return json.dumps(data, indent=2)\n        return \"OK\"\n\n\ndef run_command(socket_path: str, command: str, json_output: bool = False) -> int:\n    \"\"\"\n    Execute single command and exit.\n\n    Args:\n        socket_path: Path to control socket\n        command: Command to execute\n        json_output: If True, output raw JSON\n\n    Returns:\n        Exit code (0 for success, 1 for error)\n    \"\"\"\n    try:\n        with ControlClient(socket_path) as client:\n            cmd, args = parse_command(command)\n            full_command = f\"{cmd} {' '.join(args)}\".strip() if args else cmd\n            result = client.send_command(full_command)\n\n            if json_output:\n                print(json.dumps(result, indent=2))\n            else:\n                output = format_response(cmd, result)\n                print(output)\n\n            return 0\n\n    except ControlClientError as e:\n        print(f\"Error: {e}\", file=sys.stderr)\n        return 1\n    except KeyboardInterrupt:\n        return 130\n\n\ndef run_interactive(socket_path: str, json_output: bool = False) -> int:\n    \"\"\"\n    Run interactive CLI with readline support.\n\n    Args:\n        socket_path: Path to control socket\n        json_output: If True, output raw JSON\n\n    Returns:\n        Exit code\n    \"\"\"\n    try:\n        import readline  # noqa: F401 - imported for side effects\n        has_readline = True\n    except ImportError:\n        has_readline = False\n\n    try:\n        client = ControlClient(socket_path)\n        client.connect()\n    except ControlClientError as e:\n        print(f\"Error: {e}\", file=sys.stderr)\n        return 1\n\n    print(f\"Connected to {socket_path}\")\n    print(\"Type 'help' for available commands, 'quit' to exit.\")\n    print()\n\n    # Set up readline history\n    history_file = os.path.expanduser(\"~/.gunicornc_history\")\n    if has_readline:\n        try:\n            readline.read_history_file(history_file)\n        except FileNotFoundError:\n            pass\n\n    exit_code = 0\n\n    try:\n        while True:\n            try:\n                line = input(\"gunicorn> \").strip()\n            except EOFError:\n                print()\n                break\n\n            if not line:\n                continue\n\n            if line.lower() in ('quit', 'exit', 'q'):\n                break\n\n            try:\n                cmd, args = parse_command(line)\n                full_command = f\"{cmd} {' '.join(args)}\".strip() if args else cmd\n                result = client.send_command(full_command)\n\n                if json_output:\n                    print(json.dumps(result, indent=2))\n                else:\n                    output = format_response(cmd, result)\n                    print(output)\n\n            except ControlClientError as e:\n                print(f\"Error: {e}\")\n                # Try to reconnect\n                try:\n                    client.close()\n                    client.connect()\n                except ControlClientError:\n                    print(\"Connection lost. Exiting.\")\n                    exit_code = 1\n                    break\n\n            print()\n\n    except KeyboardInterrupt:\n        print()\n        exit_code = 130\n    finally:\n        client.close()\n        if has_readline:\n            try:\n                readline.write_history_file(history_file)\n            except Exception:\n                pass\n\n    return exit_code\n\n\ndef main():\n    \"\"\"Main entry point for gunicornc CLI.\"\"\"\n    parser = argparse.ArgumentParser(\n        description='Gunicorn control interface',\n        formatter_class=argparse.RawDescriptionHelpFormatter,\n        epilog=\"\"\"\nExamples:\n  gunicornc                      # Interactive mode (default socket)\n  gunicornc -s /tmp/myapp.ctl    # Interactive mode with custom socket\n  gunicornc -c \"show workers\"    # Single command mode\n  gunicornc -c \"worker add 2\"    # Add 2 workers\n  gunicornc -c \"show stats\" -j   # Output stats as JSON\n        \"\"\"\n    )\n\n    parser.add_argument(\n        '-s', '--socket',\n        default='gunicorn.ctl',\n        help='Control socket path (default: gunicorn.ctl in current directory)'\n    )\n\n    parser.add_argument(\n        '-c', '--command',\n        help='Execute single command and exit'\n    )\n\n    parser.add_argument(\n        '-j', '--json',\n        action='store_true',\n        help='Output raw JSON (for scripting)'\n    )\n\n    parser.add_argument(\n        '-v', '--version',\n        action='store_true',\n        help='Show version and exit'\n    )\n\n    args = parser.parse_args()\n\n    if args.version:\n        from gunicorn import __version__\n        print(f\"gunicornc (gunicorn {__version__})\")\n        return 0\n\n    socket_path = args.socket\n\n    # Make relative paths absolute from cwd\n    if not os.path.isabs(socket_path):\n        socket_path = os.path.join(os.getcwd(), socket_path)\n\n    if args.command:\n        return run_command(socket_path, args.command, args.json)\n    else:\n        return run_interactive(socket_path, args.json)\n\n\nif __name__ == '__main__':\n    sys.exit(main())\n"
  },
  {
    "path": "gunicorn/ctl/client.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nControl Socket Client\n\nClient library for connecting to gunicorn control socket.\n\"\"\"\n\nimport shlex\nimport socket\n\nfrom gunicorn.ctl.protocol import (\n    ControlProtocol,\n    make_request,\n)\n\n\nclass ControlClientError(Exception):\n    \"\"\"Control client error.\"\"\"\n\n\nclass ControlClient:\n    \"\"\"\n    Client for connecting to gunicorn control socket.\n\n    Can be used as a context manager:\n\n        with ControlClient('/path/to/gunicorn.ctl') as client:\n            result = client.send_command('show workers')\n    \"\"\"\n\n    def __init__(self, socket_path: str, timeout: float = 30.0):\n        \"\"\"\n        Initialize control client.\n\n        Args:\n            socket_path: Path to the Unix socket\n            timeout: Socket timeout in seconds (default 30)\n        \"\"\"\n        self.socket_path = socket_path\n        self.timeout = timeout\n        self._sock = None\n        self._request_id = 0\n\n    def connect(self):\n        \"\"\"\n        Connect to control socket.\n\n        Raises:\n            ControlClientError: If connection fails\n        \"\"\"\n        if self._sock:\n            return\n\n        try:\n            self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)\n            self._sock.settimeout(self.timeout)\n            self._sock.connect(self.socket_path)\n        except socket.error as e:\n            self._sock = None\n            raise ControlClientError(f\"Failed to connect to {self.socket_path}: {e}\")\n\n    def close(self):\n        \"\"\"Close connection.\"\"\"\n        if self._sock:\n            try:\n                self._sock.close()\n            except Exception:\n                pass\n            self._sock = None\n\n    def send_command(self, command: str, args: list = None) -> dict:\n        \"\"\"\n        Send command and wait for response.\n\n        Args:\n            command: Command string (e.g., \"show workers\")\n            args: Optional additional arguments\n\n        Returns:\n            Response data dictionary\n\n        Raises:\n            ControlClientError: If communication fails\n        \"\"\"\n        if not self._sock:\n            self.connect()\n\n        self._request_id += 1\n        request = make_request(self._request_id, command, args)\n\n        try:\n            ControlProtocol.write_message(self._sock, request)\n            response = ControlProtocol.read_message(self._sock)\n        except Exception as e:\n            self.close()\n            raise ControlClientError(f\"Communication error: {e}\")\n\n        if response.get(\"status\") == \"error\":\n            raise ControlClientError(response.get(\"error\", \"Unknown error\"))\n\n        return response.get(\"data\", {})\n\n    def __enter__(self):\n        self.connect()\n        return self\n\n    def __exit__(self, *args):\n        self.close()\n\n\ndef parse_command(line: str) -> tuple:\n    \"\"\"\n    Parse a command line into command and args.\n\n    Args:\n        line: Command line string\n\n    Returns:\n        Tuple of (command_string, args_list)\n    \"\"\"\n    parts = shlex.split(line)\n    if not parts:\n        return \"\", []\n\n    # Find where numeric/value args start\n    command_parts = []\n    args = []\n\n    for part in parts:\n        # If we haven't hit args yet and this looks like a command word\n        if not args and not part.isdigit() and not part.startswith('-'):\n            command_parts.append(part)\n        else:\n            args.append(part)\n\n    return \" \".join(command_parts), args\n"
  },
  {
    "path": "gunicorn/ctl/handlers.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nControl Interface Command Handlers\n\nProvides handlers for all control commands with access to arbiter state.\n\"\"\"\n\nimport os\nimport signal\nimport socket\nimport time\n\n\nclass CommandHandlers:\n    \"\"\"\n    Command handlers with access to arbiter state.\n\n    All handler methods return dictionaries that will be sent\n    as the response data.\n    \"\"\"\n\n    def __init__(self, arbiter):\n        \"\"\"\n        Initialize handlers with arbiter reference.\n\n        Args:\n            arbiter: The Gunicorn arbiter instance\n        \"\"\"\n        self.arbiter = arbiter\n\n    def show_workers(self) -> dict:\n        \"\"\"\n        Return list of HTTP workers.\n\n        Returns:\n            Dictionary with workers list containing:\n            - pid: Worker process ID\n            - age: Worker age (spawn order)\n            - requests: Number of requests handled (if available)\n            - booted: Whether worker has finished booting\n            - last_heartbeat: Seconds since last heartbeat\n        \"\"\"\n        workers = []\n        now = time.monotonic()\n\n        for pid, worker in self.arbiter.WORKERS.items():\n            try:\n                last_update = worker.tmp.last_update()\n                last_heartbeat = round(now - last_update, 2)\n            except (OSError, ValueError):\n                last_heartbeat = None\n\n            workers.append({\n                \"pid\": pid,\n                \"age\": worker.age,\n                \"booted\": worker.booted,\n                \"aborted\": worker.aborted,\n                \"last_heartbeat\": last_heartbeat,\n            })\n\n        # Sort by age (oldest first)\n        workers.sort(key=lambda w: w[\"age\"])\n\n        return {\"workers\": workers, \"count\": len(workers)}\n\n    def show_dirty(self) -> dict:\n        \"\"\"\n        Return dirty workers and apps information.\n\n        Returns:\n            Dictionary with:\n            - enabled: Whether dirty arbiter is running\n            - pid: Dirty arbiter PID\n            - workers: List of dirty worker info\n            - apps: List of dirty app specs\n        \"\"\"\n        if not self.arbiter.dirty_arbiter_pid:\n            return {\n                \"enabled\": False,\n                \"pid\": None,\n                \"workers\": [],\n                \"apps\": [],\n            }\n\n        # Get dirty arbiter reference if available\n        dirty_arbiter = getattr(self.arbiter, 'dirty_arbiter', None)\n\n        workers = []\n        apps = []\n\n        if dirty_arbiter and hasattr(dirty_arbiter, 'workers'):\n            now = time.monotonic()\n            for pid, worker in dirty_arbiter.workers.items():\n                try:\n                    last_update = worker.tmp.last_update()\n                    last_heartbeat = round(now - last_update, 2)\n                except (OSError, ValueError, AttributeError):\n                    last_heartbeat = None\n\n                workers.append({\n                    \"pid\": pid,\n                    \"age\": worker.age,\n                    \"apps\": getattr(worker, 'app_paths', []),\n                    \"booted\": getattr(worker, 'booted', False),\n                    \"last_heartbeat\": last_heartbeat,\n                })\n\n            # Get app specs\n            if hasattr(dirty_arbiter, 'app_specs'):\n                for path, spec in dirty_arbiter.app_specs.items():\n                    worker_pids = list(dirty_arbiter.app_worker_map.get(path, []))\n                    apps.append({\n                        \"import_path\": path,\n                        \"worker_count\": spec.get('worker_count'),\n                        \"current_workers\": len(worker_pids),\n                        \"worker_pids\": worker_pids,\n                    })\n\n        return {\n            \"enabled\": True,\n            \"pid\": self.arbiter.dirty_arbiter_pid,\n            \"workers\": workers,\n            \"apps\": apps,\n        }\n\n    def show_config(self) -> dict:\n        \"\"\"\n        Return current effective configuration.\n\n        Returns:\n            Dictionary of configuration values\n        \"\"\"\n        cfg = self.arbiter.cfg\n        config = {}\n\n        # Get commonly needed config values\n        config_keys = [\n            'bind', 'workers', 'worker_class', 'threads', 'timeout',\n            'graceful_timeout', 'keepalive', 'max_requests',\n            'max_requests_jitter', 'worker_connections', 'preload_app',\n            'daemon', 'pidfile', 'proc_name', 'reload',\n            'dirty_workers', 'dirty_apps', 'dirty_timeout',\n            'control_socket', 'control_socket_disable',\n        ]\n\n        for key in config_keys:\n            try:\n                value = getattr(cfg, key)\n                # Convert non-serializable types\n                if callable(value):\n                    value = str(value)\n                elif hasattr(value, '__class__') and not isinstance(\n                        value, (str, int, float, bool, list, dict, type(None))):\n                    value = str(value)\n                config[key] = value\n            except AttributeError:\n                pass\n\n        return config\n\n    def show_stats(self) -> dict:\n        \"\"\"\n        Return server statistics.\n\n        Returns:\n            Dictionary with:\n            - uptime: Seconds since arbiter started\n            - pid: Arbiter PID\n            - workers_current: Current number of workers\n            - workers_spawned: Total workers spawned\n            - workers_killed: Total workers killed (if tracked)\n            - reloads: Number of reloads (if tracked)\n        \"\"\"\n        stats = getattr(self.arbiter, '_stats', {})\n        start_time = stats.get('start_time')\n\n        uptime = None\n        if start_time:\n            uptime = round(time.time() - start_time, 2)\n\n        return {\n            \"uptime\": uptime,\n            \"pid\": self.arbiter.pid,\n            \"workers_current\": len(self.arbiter.WORKERS),\n            \"workers_target\": self.arbiter.num_workers,\n            \"workers_spawned\": stats.get('workers_spawned', 0),\n            \"workers_killed\": stats.get('workers_killed', 0),\n            \"reloads\": stats.get('reloads', 0),\n            \"dirty_arbiter_pid\": self.arbiter.dirty_arbiter_pid or None,\n        }\n\n    def show_listeners(self) -> dict:\n        \"\"\"\n        Return bound socket information.\n\n        Returns:\n            Dictionary with listeners list\n        \"\"\"\n        listeners = []\n\n        for lnr in self.arbiter.LISTENERS:\n            addr = str(lnr)\n            listener_info = {\n                \"address\": addr,\n                \"fd\": lnr.fileno(),\n            }\n\n            # Try to get socket family\n            try:\n                sock = lnr.sock\n                if sock.family == socket.AF_UNIX:\n                    listener_info[\"type\"] = \"unix\"\n                elif sock.family == socket.AF_INET:\n                    listener_info[\"type\"] = \"tcp\"\n                elif sock.family == socket.AF_INET6:\n                    listener_info[\"type\"] = \"tcp6\"\n            except Exception:\n                listener_info[\"type\"] = \"unknown\"\n\n            listeners.append(listener_info)\n\n        return {\"listeners\": listeners, \"count\": len(listeners)}\n\n    def worker_add(self, count: int = 1) -> dict:\n        \"\"\"\n        Increase worker count.\n\n        Args:\n            count: Number of workers to add (default 1)\n\n        Returns:\n            Dictionary with added count and new total\n        \"\"\"\n        count = max(1, int(count))\n        old_count = self.arbiter.num_workers\n        self.arbiter.num_workers += count\n\n        # Wake up the arbiter to spawn workers\n        self.arbiter.wakeup()\n\n        return {\n            \"added\": count,\n            \"previous\": old_count,\n            \"total\": self.arbiter.num_workers,\n        }\n\n    def worker_remove(self, count: int = 1) -> dict:\n        \"\"\"\n        Decrease worker count.\n\n        Args:\n            count: Number of workers to remove (default 1)\n\n        Returns:\n            Dictionary with removed count and new total\n        \"\"\"\n        count = max(1, int(count))\n        old_count = self.arbiter.num_workers\n\n        # Don't go below 1 worker\n        new_count = max(1, old_count - count)\n        actual_removed = old_count - new_count\n\n        self.arbiter.num_workers = new_count\n\n        # Wake up the arbiter to kill excess workers\n        self.arbiter.wakeup()\n\n        return {\n            \"removed\": actual_removed,\n            \"previous\": old_count,\n            \"total\": new_count,\n        }\n\n    def worker_kill(self, pid: int) -> dict:\n        \"\"\"\n        Gracefully terminate a specific worker.\n\n        Args:\n            pid: Worker process ID\n\n        Returns:\n            Dictionary with killed PID or error\n        \"\"\"\n        pid = int(pid)\n\n        if pid not in self.arbiter.WORKERS:\n            return {\n                \"success\": False,\n                \"error\": f\"Worker {pid} not found\",\n            }\n\n        try:\n            os.kill(pid, signal.SIGTERM)\n            return {\n                \"success\": True,\n                \"killed\": pid,\n            }\n        except OSError as e:\n            return {\n                \"success\": False,\n                \"error\": str(e),\n            }\n\n    def dirty_add(self, count: int = 1) -> dict:\n        \"\"\"\n        Spawn additional dirty workers.\n\n        Sends a MANAGE message to the dirty arbiter to spawn workers.\n\n        Args:\n            count: Number of dirty workers to add (default 1)\n\n        Returns:\n            Dictionary with added count or error\n        \"\"\"\n        if not self.arbiter.dirty_arbiter_pid:\n            return {\n                \"success\": False,\n                \"error\": \"Dirty arbiter not running\",\n            }\n\n        count = max(1, int(count))\n        return self._send_manage_message(\"add\", count)\n\n    def dirty_remove(self, count: int = 1) -> dict:\n        \"\"\"\n        Remove dirty workers.\n\n        Sends a MANAGE message to the dirty arbiter to remove workers.\n\n        Args:\n            count: Number of dirty workers to remove (default 1)\n\n        Returns:\n            Dictionary with removed count or error\n        \"\"\"\n        if not self.arbiter.dirty_arbiter_pid:\n            return {\n                \"success\": False,\n                \"error\": \"Dirty arbiter not running\",\n            }\n\n        count = max(1, int(count))\n        return self._send_manage_message(\"remove\", count)\n\n    def _send_manage_message(self, operation: str, count: int) -> dict:\n        \"\"\"\n        Send a worker management message to the dirty arbiter.\n\n        Args:\n            operation: \"add\" or \"remove\"\n            count: Number of workers to add/remove\n\n        Returns:\n            Dictionary with result or error\n        \"\"\"\n        # Get socket path from arbiter object or environment\n        dirty_socket_path = None\n        if hasattr(self.arbiter, 'dirty_arbiter') and self.arbiter.dirty_arbiter:\n            dirty_socket_path = getattr(\n                self.arbiter.dirty_arbiter, 'socket_path', None\n            )\n        if not dirty_socket_path:\n            dirty_socket_path = os.environ.get('GUNICORN_DIRTY_SOCKET')\n        if not dirty_socket_path:\n            return {\n                \"success\": False,\n                \"error\": \"Cannot find dirty arbiter socket path\",\n            }\n\n        try:\n            from gunicorn.dirty.protocol import (\n                DirtyProtocol, MANAGE_OP_ADD, MANAGE_OP_REMOVE\n            )\n\n            op = MANAGE_OP_ADD if operation == \"add\" else MANAGE_OP_REMOVE\n\n            sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)\n            sock.settimeout(10.0)\n            sock.connect(dirty_socket_path)\n\n            # Send manage request\n            request = {\n                \"type\": DirtyProtocol.MSG_TYPE_MANAGE,\n                \"id\": 1,\n                \"op\": op,\n                \"count\": count,\n            }\n            DirtyProtocol.write_message(sock, request)\n\n            # Read response\n            response = DirtyProtocol.read_message(sock)\n            sock.close()\n\n            if response.get(\"type\") == DirtyProtocol.MSG_TYPE_RESPONSE:\n                return response.get(\"result\", {\"success\": True})\n            elif response.get(\"type\") == DirtyProtocol.MSG_TYPE_ERROR:\n                error = response.get(\"error\", {})\n                return {\n                    \"success\": False,\n                    \"error\": error.get(\"message\", str(error)),\n                }\n            else:\n                return {\n                    \"success\": False,\n                    \"error\": f\"Unexpected response type: {response.get('type')}\",\n                }\n\n        except Exception as e:\n            return {\n                \"success\": False,\n                \"error\": str(e),\n            }\n\n    def reload(self) -> dict:\n        \"\"\"\n        Trigger graceful reload (equivalent to SIGHUP).\n\n        Returns:\n            Dictionary with status\n        \"\"\"\n        # Send HUP to self to trigger reload\n        os.kill(self.arbiter.pid, signal.SIGHUP)\n        return {\"status\": \"reloading\"}\n\n    def reopen(self) -> dict:\n        \"\"\"\n        Reopen log files (equivalent to SIGUSR1).\n\n        Returns:\n            Dictionary with status\n        \"\"\"\n        os.kill(self.arbiter.pid, signal.SIGUSR1)\n        return {\"status\": \"reopening\"}\n\n    def shutdown(self, mode: str = \"graceful\") -> dict:\n        \"\"\"\n        Initiate shutdown.\n\n        Args:\n            mode: \"graceful\" (SIGTERM) or \"quick\" (SIGINT)\n\n        Returns:\n            Dictionary with status\n        \"\"\"\n        if mode == \"quick\":\n            os.kill(self.arbiter.pid, signal.SIGINT)\n        else:\n            os.kill(self.arbiter.pid, signal.SIGTERM)\n\n        return {\"status\": \"shutting_down\", \"mode\": mode}\n\n    def show_all(self) -> dict:\n        \"\"\"\n        Return overview of all processes (arbiter, web workers, dirty arbiter, dirty workers).\n\n        Returns:\n            Dictionary with complete process hierarchy\n        \"\"\"\n        now = time.monotonic()\n\n        # Arbiter info\n        arbiter_info = {\n            \"pid\": self.arbiter.pid,\n            \"type\": \"arbiter\",\n            \"role\": \"master\",\n        }\n\n        # Web workers (HTTP workers)\n        web_workers = []\n        for pid, worker in self.arbiter.WORKERS.items():\n            try:\n                last_update = worker.tmp.last_update()\n                last_heartbeat = round(now - last_update, 2)\n            except (OSError, ValueError):\n                last_heartbeat = None\n\n            web_workers.append({\n                \"pid\": pid,\n                \"type\": \"web\",\n                \"age\": worker.age,\n                \"booted\": worker.booted,\n                \"last_heartbeat\": last_heartbeat,\n            })\n\n        # Sort by age\n        web_workers.sort(key=lambda w: w[\"age\"])\n\n        # Dirty arbiter info (runs in separate process)\n        dirty_arbiter_info = None\n        dirty_workers = []\n\n        if self.arbiter.dirty_arbiter_pid:\n            dirty_arbiter_info = {\n                \"pid\": self.arbiter.dirty_arbiter_pid,\n                \"type\": \"dirty_arbiter\",\n                \"role\": \"dirty master\",\n            }\n\n            # Query dirty arbiter for worker info via its socket\n            dirty_workers = self._query_dirty_workers()\n\n        return {\n            \"arbiter\": arbiter_info,\n            \"web_workers\": web_workers,\n            \"web_worker_count\": len(web_workers),\n            \"dirty_arbiter\": dirty_arbiter_info,\n            \"dirty_workers\": dirty_workers,\n            \"dirty_worker_count\": len(dirty_workers),\n        }\n\n    def _query_dirty_workers(self) -> list:\n        \"\"\"\n        Query the dirty arbiter for worker information.\n\n        Connects to the dirty arbiter socket and sends a status request.\n\n        Returns:\n            List of dirty worker info dicts, or empty list on error\n        \"\"\"\n        # Get socket path from arbiter object or environment\n        dirty_socket_path = None\n        if hasattr(self.arbiter, 'dirty_arbiter') and self.arbiter.dirty_arbiter:\n            dirty_socket_path = getattr(self.arbiter.dirty_arbiter, 'socket_path', None)\n        if not dirty_socket_path:\n            dirty_socket_path = os.environ.get('GUNICORN_DIRTY_SOCKET')\n        if not dirty_socket_path:\n            return []\n\n        try:\n            from gunicorn.dirty.protocol import DirtyProtocol\n\n            sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)\n            sock.settimeout(2.0)\n            sock.connect(dirty_socket_path)\n\n            # Send status request\n            request = {\n                \"type\": DirtyProtocol.MSG_TYPE_STATUS,\n                \"id\": \"ctl-status-1\",\n            }\n            DirtyProtocol.write_message(sock, request)\n\n            # Read response\n            response = DirtyProtocol.read_message(sock)\n            sock.close()\n\n            if response.get(\"type\") == DirtyProtocol.MSG_TYPE_RESPONSE:\n                result = response.get(\"result\", {})\n                return result.get(\"workers\", [])\n\n        except Exception:\n            pass\n\n        return []\n\n    def help(self) -> dict:\n        \"\"\"\n        Return list of available commands.\n\n        Returns:\n            Dictionary with commands and descriptions\n        \"\"\"\n        commands = {\n            \"show all\": \"Show all processes (arbiter, web workers, dirty workers)\",\n            \"show workers\": \"List HTTP workers with their status\",\n            \"show dirty\": \"List dirty workers and apps\",\n            \"show config\": \"Show current effective configuration\",\n            \"show stats\": \"Show server statistics\",\n            \"show listeners\": \"Show bound sockets\",\n            \"worker add [N]\": \"Spawn N workers (default 1)\",\n            \"worker remove [N]\": \"Remove N workers (default 1)\",\n            \"worker kill <PID>\": \"Gracefully terminate specific worker\",\n            \"dirty add [N]\": \"Spawn N dirty workers (default 1)\",\n            \"dirty remove [N]\": \"Remove N dirty workers (default 1)\",\n            \"reload\": \"Graceful reload (HUP)\",\n            \"reopen\": \"Reopen log files (USR1)\",\n            \"shutdown [graceful|quick]\": \"Shutdown server (TERM/INT)\",\n            \"help\": \"Show this help message\",\n        }\n        return {\"commands\": commands}\n"
  },
  {
    "path": "gunicorn/ctl/protocol.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nControl Socket Protocol\n\nJSON-based protocol with length-prefixed framing for the control interface.\n\nMessage Format:\n    +----------------+------------------+\n    | Length (4B BE) |  JSON Payload    |\n    +----------------+------------------+\n\nRequest Format:\n    {\"id\": 1, \"command\": \"show\", \"args\": [\"workers\"]}\n\nResponse Format:\n    {\"id\": 1, \"status\": \"ok\", \"data\": {...}}\n    {\"id\": 1, \"status\": \"error\", \"error\": \"message\"}\n\"\"\"\n\nimport json\nimport struct\n\n\nclass ProtocolError(Exception):\n    \"\"\"Protocol-level error.\"\"\"\n\n\nclass ControlProtocol:\n    \"\"\"\n    Protocol implementation for control socket communication.\n\n    Uses 4-byte big-endian length prefix followed by JSON payload.\n    \"\"\"\n\n    # Maximum message size (16 MB)\n    MAX_MESSAGE_SIZE = 16 * 1024 * 1024\n\n    @staticmethod\n    def encode_message(data: dict) -> bytes:\n        \"\"\"\n        Encode a message for transmission.\n\n        Args:\n            data: Dictionary to encode\n\n        Returns:\n            Length-prefixed JSON bytes\n        \"\"\"\n        payload = json.dumps(data).encode('utf-8')\n        length = struct.pack('>I', len(payload))\n        return length + payload\n\n    @staticmethod\n    def decode_message(data: bytes) -> dict:\n        \"\"\"\n        Decode a message from bytes.\n\n        Args:\n            data: Raw bytes (length prefix + JSON payload)\n\n        Returns:\n            Decoded dictionary\n        \"\"\"\n        if len(data) < 4:\n            raise ProtocolError(\"Message too short\")\n\n        length = struct.unpack('>I', data[:4])[0]\n        if len(data) < 4 + length:\n            raise ProtocolError(\"Incomplete message\")\n\n        payload = data[4:4 + length]\n        return json.loads(payload.decode('utf-8'))\n\n    @staticmethod\n    def read_message(sock) -> dict:\n        \"\"\"\n        Read one message from a socket.\n\n        Args:\n            sock: Socket to read from\n\n        Returns:\n            Decoded message dictionary\n\n        Raises:\n            ProtocolError: If message is malformed\n            ConnectionError: If connection is closed\n        \"\"\"\n        # Read length prefix\n        length_data = b''\n        while len(length_data) < 4:\n            chunk = sock.recv(4 - len(length_data))\n            if not chunk:\n                if not length_data:\n                    raise ConnectionError(\"Connection closed\")\n                raise ProtocolError(\"Incomplete length prefix\")\n            length_data += chunk\n\n        length = struct.unpack('>I', length_data)[0]\n\n        if length > ControlProtocol.MAX_MESSAGE_SIZE:\n            raise ProtocolError(f\"Message too large: {length}\")\n\n        # Read payload\n        payload_data = b''\n        while len(payload_data) < length:\n            chunk = sock.recv(min(length - len(payload_data), 65536))\n            if not chunk:\n                raise ProtocolError(\"Incomplete payload\")\n            payload_data += chunk\n\n        try:\n            return json.loads(payload_data.decode('utf-8'))\n        except json.JSONDecodeError as e:\n            raise ProtocolError(f\"Invalid JSON: {e}\")\n\n    @staticmethod\n    def write_message(sock, data: dict):\n        \"\"\"\n        Write one message to a socket.\n\n        Args:\n            sock: Socket to write to\n            data: Message dictionary to send\n        \"\"\"\n        message = ControlProtocol.encode_message(data)\n        sock.sendall(message)\n\n    @staticmethod\n    async def read_message_async(reader) -> dict:\n        \"\"\"\n        Read one message from an async reader.\n\n        Args:\n            reader: asyncio StreamReader\n\n        Returns:\n            Decoded message dictionary\n        \"\"\"\n        # Read length prefix\n        length_data = await reader.readexactly(4)\n        length = struct.unpack('>I', length_data)[0]\n\n        if length > ControlProtocol.MAX_MESSAGE_SIZE:\n            raise ProtocolError(f\"Message too large: {length}\")\n\n        # Read payload\n        payload_data = await reader.readexactly(length)\n\n        try:\n            return json.loads(payload_data.decode('utf-8'))\n        except json.JSONDecodeError as e:\n            raise ProtocolError(f\"Invalid JSON: {e}\")\n\n    @staticmethod\n    async def write_message_async(writer, data: dict):\n        \"\"\"\n        Write one message to an async writer.\n\n        Args:\n            writer: asyncio StreamWriter\n            data: Message dictionary to send\n        \"\"\"\n        message = ControlProtocol.encode_message(data)\n        writer.write(message)\n        await writer.drain()\n\n\ndef make_request(request_id: int, command: str, args: list = None) -> dict:\n    \"\"\"\n    Create a request message.\n\n    Args:\n        request_id: Unique request identifier\n        command: Command name (e.g., \"show workers\")\n        args: Optional list of arguments\n\n    Returns:\n        Request dictionary\n    \"\"\"\n    return {\n        \"id\": request_id,\n        \"command\": command,\n        \"args\": args or [],\n    }\n\n\ndef make_response(request_id: int, data: dict = None) -> dict:\n    \"\"\"\n    Create a success response message.\n\n    Args:\n        request_id: Request identifier being responded to\n        data: Response data\n\n    Returns:\n        Response dictionary\n    \"\"\"\n    return {\n        \"id\": request_id,\n        \"status\": \"ok\",\n        \"data\": data or {},\n    }\n\n\ndef make_error_response(request_id: int, error: str) -> dict:\n    \"\"\"\n    Create an error response message.\n\n    Args:\n        request_id: Request identifier being responded to\n        error: Error message\n\n    Returns:\n        Error response dictionary\n    \"\"\"\n    return {\n        \"id\": request_id,\n        \"status\": \"error\",\n        \"error\": error,\n    }\n"
  },
  {
    "path": "gunicorn/ctl/server.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nControl Socket Server\n\nRuns in the arbiter process and accepts commands via Unix socket.\nUses asyncio in a background thread to handle client connections.\n\nFork Safety:\n    This server uses os.register_at_fork() to properly handle fork() calls.\n    Before fork: the asyncio thread is stopped to prevent lock issues.\n    After fork in parent: the server is restarted.\n    After fork in child: references are cleared (workers don't need the control server).\n\"\"\"\n\nimport asyncio\nimport os\nimport shlex\nimport threading\n\nfrom gunicorn.ctl.handlers import CommandHandlers\nfrom gunicorn.ctl.protocol import (\n    ControlProtocol,\n    make_response,\n    make_error_response,\n)\n\n\n# Module-level tracking of active control server instances for fork handling.\n# This is necessary because os.register_at_fork() callbacks are process-level.\n_active_servers = set()\n_module_state = {\"fork_handlers_registered\": False}\n\n\ndef _register_fork_handlers():\n    \"\"\"Register fork handlers once at module level.\"\"\"\n    if _module_state[\"fork_handlers_registered\"]:\n        return\n    _module_state[\"fork_handlers_registered\"] = True\n\n    os.register_at_fork(\n        before=_before_fork,\n        after_in_parent=_after_fork_parent,\n        after_in_child=_after_fork_child,\n    )\n\n\ndef _before_fork():\n    \"\"\"Called before fork() - stop all active control servers.\"\"\"\n    for server in list(_active_servers):\n        server._stop_for_fork()\n\n\ndef _after_fork_parent():\n    \"\"\"Called in parent after fork() - restart all control servers.\"\"\"\n    for server in list(_active_servers):\n        server._restart_after_fork()\n\n\ndef _after_fork_child():\n    \"\"\"Called in child after fork() - cleanup references.\"\"\"\n    # In the child process (worker), we don't need the control server.\n    # Just clear the references without trying to stop anything.\n    _active_servers.clear()\n\n\nclass ControlSocketServer:\n    \"\"\"\n    Control socket server running in arbiter process.\n\n    The server runs an asyncio event loop in a background thread,\n    accepting connections and dispatching commands to handlers.\n\n    Fork safety is handled via os.register_at_fork() - the server\n    automatically stops before fork and restarts after in the parent.\n    \"\"\"\n\n    def __init__(self, arbiter, socket_path, socket_mode=0o600):\n        \"\"\"\n        Initialize control socket server.\n\n        Args:\n            arbiter: The Gunicorn arbiter instance\n            socket_path: Path for the Unix socket\n            socket_mode: Permission mode for socket (default 0o600)\n        \"\"\"\n        self.arbiter = arbiter\n        self.socket_path = socket_path\n        self.socket_mode = socket_mode\n\n        self.handlers = CommandHandlers(arbiter)\n        self._server = None\n        self._loop = None\n        self._thread = None\n        self._running = False\n        self._was_running_before_fork = False\n\n        # Ensure fork handlers are registered\n        _register_fork_handlers()\n\n    def start(self):\n        \"\"\"Start server in background thread with asyncio event loop.\"\"\"\n        if self._running:\n            return\n\n        self._running = True\n        self._thread = threading.Thread(target=self._run_loop, daemon=True)\n        self._thread.start()\n\n        # Track this server for fork handling\n        _active_servers.add(self)\n\n    def stop(self):\n        \"\"\"Stop server and cleanup socket.\"\"\"\n        # Remove from active servers tracking\n        _active_servers.discard(self)\n\n        if not self._running:\n            return\n\n        self._running = False\n\n        if self._loop and self._server:\n            # Schedule server close in the loop\n            self._loop.call_soon_threadsafe(self._shutdown)\n\n        if self._thread:\n            self._thread.join(timeout=2.0)\n            self._thread = None\n\n        # Clean up socket file\n        if os.path.exists(self.socket_path):\n            try:\n                os.unlink(self.socket_path)\n            except OSError:\n                pass\n\n    def _stop_for_fork(self):\n        \"\"\"Stop server before fork (called by fork handler).\"\"\"\n        if not self._running:\n            self._was_running_before_fork = False\n            return\n\n        self._was_running_before_fork = True\n        self._running = False\n\n        if self._loop and self._server:\n            try:\n                self._loop.call_soon_threadsafe(self._shutdown)\n            except RuntimeError:\n                # Loop may already be closed\n                pass\n\n        if self._thread:\n            self._thread.join(timeout=2.0)\n            self._thread = None\n\n        self._loop = None\n        self._server = None\n\n    def _restart_after_fork(self):\n        \"\"\"Restart server in parent after fork (called by fork handler).\"\"\"\n        if not self._was_running_before_fork:\n            return\n\n        self._was_running_before_fork = False\n        self._running = True\n        self._thread = threading.Thread(target=self._run_loop, daemon=True)\n        self._thread.start()\n\n    def _shutdown(self):\n        \"\"\"Shutdown server (called from event loop thread).\"\"\"\n        if self._server:\n            self._server.close()\n\n    def _run_loop(self):\n        \"\"\"Run the asyncio event loop in background thread.\"\"\"\n        try:\n            asyncio.run(self._serve())\n        except Exception as e:\n            if self._running and self.arbiter.log:\n                self.arbiter.log.error(\"Control server error: %s\", e)\n\n    async def _serve(self):\n        \"\"\"Main async server loop.\"\"\"\n        self._loop = asyncio.get_running_loop()\n\n        # Remove socket if it exists\n        if os.path.exists(self.socket_path):\n            os.unlink(self.socket_path)\n\n        # Create Unix socket server\n        self._server = await asyncio.start_unix_server(\n            self._handle_client,\n            path=self.socket_path\n        )\n\n        # Set socket permissions\n        os.chmod(self.socket_path, self.socket_mode)\n\n        if self.arbiter.log:\n            self.arbiter.log.info(\"Control socket listening at %s\",\n                                  self.socket_path)\n\n        try:\n            async with self._server:\n                await self._server.serve_forever()\n        except asyncio.CancelledError:\n            pass\n        finally:\n            if os.path.exists(self.socket_path):\n                try:\n                    os.unlink(self.socket_path)\n                except OSError:\n                    pass\n\n    async def _handle_client(self, reader, writer):\n        \"\"\"\n        Handle client connection.\n\n        Args:\n            reader: asyncio StreamReader\n            writer: asyncio StreamWriter\n        \"\"\"\n        try:\n            while self._running:\n                try:\n                    message = await asyncio.wait_for(\n                        ControlProtocol.read_message_async(reader),\n                        timeout=300.0  # 5 minute idle timeout\n                    )\n                except asyncio.TimeoutError:\n                    # Client idle too long, close connection\n                    break\n                except asyncio.IncompleteReadError:\n                    # Client disconnected\n                    break\n                except Exception:\n                    # Protocol error\n                    break\n\n                # Process command\n                response = await self._dispatch(message)\n\n                # Send response\n                await ControlProtocol.write_message_async(writer, response)\n\n        except Exception as e:\n            if self.arbiter.log:\n                self.arbiter.log.debug(\"Control client error: %s\", e)\n        finally:\n            writer.close()\n            try:\n                await writer.wait_closed()\n            except Exception:\n                pass\n\n    async def _dispatch(self, message: dict) -> dict:\n        \"\"\"\n        Dispatch command to appropriate handler.\n\n        Args:\n            message: Request message dict\n\n        Returns:\n            Response dictionary\n        \"\"\"\n        request_id = message.get(\"id\", 0)\n        command = message.get(\"command\", \"\").strip()\n        args = message.get(\"args\", [])\n\n        if not command:\n            return make_error_response(request_id, \"Empty command\")\n\n        try:\n            # Parse command (e.g., \"show workers\" or \"worker add 2\")\n            parts = shlex.split(command)\n            if args:\n                parts.extend(str(a) for a in args)\n\n            if not parts:\n                return make_error_response(request_id, \"Empty command\")\n\n            # Route to handler\n            result = self._execute_command(parts)\n            return make_response(request_id, result)\n\n        except ValueError as e:\n            return make_error_response(request_id, f\"Invalid argument: {e}\")\n        except Exception as e:\n            if self.arbiter.log:\n                self.arbiter.log.exception(\"Command error\")\n            return make_error_response(request_id, f\"Command failed: {e}\")\n\n    def _execute_command(self, parts: list) -> dict:  # pylint: disable=too-many-return-statements\n        \"\"\"\n        Execute a parsed command.\n\n        Args:\n            parts: Command parts (e.g., [\"show\", \"workers\"])\n\n        Returns:\n            Handler result dictionary\n        \"\"\"\n        if not parts:\n            raise ValueError(\"Empty command\")\n\n        cmd = parts[0].lower()\n        rest = parts[1:]\n\n        # Map commands to handlers\n        if cmd == \"show\":\n            return self._handle_show(rest)\n        elif cmd == \"worker\":\n            return self._handle_worker(rest)\n        elif cmd == \"dirty\":\n            return self._handle_dirty(rest)\n        elif cmd == \"reload\":\n            return self.handlers.reload()\n        elif cmd == \"reopen\":\n            return self.handlers.reopen()\n        elif cmd == \"shutdown\":\n            mode = rest[0] if rest else \"graceful\"\n            return self.handlers.shutdown(mode)\n        elif cmd == \"help\":\n            return self.handlers.help()\n        else:\n            raise ValueError(f\"Unknown command: {cmd}\")\n\n    def _handle_show(self, args: list) -> dict:\n        \"\"\"Handle 'show' commands.\"\"\"\n        if not args:\n            raise ValueError(\"Missing show target (all|workers|dirty|config|stats|listeners)\")\n\n        target = args[0].lower()\n\n        if target == \"all\":\n            return self.handlers.show_all()\n        elif target == \"workers\":\n            return self.handlers.show_workers()\n        elif target == \"dirty\":\n            return self.handlers.show_dirty()\n        elif target == \"config\":\n            return self.handlers.show_config()\n        elif target == \"stats\":\n            return self.handlers.show_stats()\n        elif target == \"listeners\":\n            return self.handlers.show_listeners()\n        else:\n            raise ValueError(f\"Unknown show target: {target}\")\n\n    def _handle_worker(self, args: list) -> dict:\n        \"\"\"Handle 'worker' commands.\"\"\"\n        if not args:\n            raise ValueError(\"Missing worker action (add|remove|kill)\")\n\n        action = args[0].lower()\n        action_args = args[1:]\n\n        if action == \"add\":\n            count = int(action_args[0]) if action_args else 1\n            return self.handlers.worker_add(count)\n        elif action == \"remove\":\n            count = int(action_args[0]) if action_args else 1\n            return self.handlers.worker_remove(count)\n        elif action == \"kill\":\n            if not action_args:\n                raise ValueError(\"Missing PID for worker kill\")\n            pid = int(action_args[0])\n            return self.handlers.worker_kill(pid)\n        else:\n            raise ValueError(f\"Unknown worker action: {action}\")\n\n    def _handle_dirty(self, args: list) -> dict:\n        \"\"\"Handle 'dirty' commands.\"\"\"\n        if not args:\n            raise ValueError(\"Missing dirty action (add|remove)\")\n\n        action = args[0].lower()\n        action_args = args[1:]\n\n        if action == \"add\":\n            count = int(action_args[0]) if action_args else 1\n            return self.handlers.dirty_add(count)\n        elif action == \"remove\":\n            count = int(action_args[0]) if action_args else 1\n            return self.handlers.dirty_remove(count)\n        else:\n            raise ValueError(f\"Unknown dirty action: {action}\")\n"
  },
  {
    "path": "gunicorn/debug.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"The debug module contains utilities and functions for better\ndebugging Gunicorn.\"\"\"\n\nimport sys\nimport linecache\nimport re\nimport inspect\n\n__all__ = ['spew', 'unspew']\n\n_token_spliter = re.compile(r'\\W+')\n\n\nclass Spew:\n\n    def __init__(self, trace_names=None, show_values=True):\n        self.trace_names = trace_names\n        self.show_values = show_values\n\n    def __call__(self, frame, event, arg):\n        if event == 'line':\n            lineno = frame.f_lineno\n            if '__file__' in frame.f_globals:\n                filename = frame.f_globals['__file__']\n                if (filename.endswith('.pyc') or\n                        filename.endswith('.pyo')):\n                    filename = filename[:-1]\n                name = frame.f_globals['__name__']\n                line = linecache.getline(filename, lineno)\n            else:\n                name = '[unknown]'\n                try:\n                    src = inspect.getsourcelines(frame)\n                    line = src[lineno]\n                except OSError:\n                    line = 'Unknown code named [%s].  VM instruction #%d' % (\n                        frame.f_code.co_name, frame.f_lasti)\n            if self.trace_names is None or name in self.trace_names:\n                print('%s:%s: %s' % (name, lineno, line.rstrip()))\n                if not self.show_values:\n                    return self\n                details = []\n                tokens = _token_spliter.split(line)\n                for tok in tokens:\n                    if tok in frame.f_globals:\n                        details.append('%s=%r' % (tok, frame.f_globals[tok]))\n                    if tok in frame.f_locals:\n                        details.append('%s=%r' % (tok, frame.f_locals[tok]))\n                if details:\n                    print(\"\\t%s\" % ' '.join(details))\n        return self\n\n\ndef spew(trace_names=None, show_values=False):\n    \"\"\"Install a trace hook which writes incredibly detailed logs\n    about what code is being executed to stdout.\n    \"\"\"\n    sys.settrace(Spew(trace_names, show_values))\n\n\ndef unspew():\n    \"\"\"Remove the trace hook installed by spew.\n    \"\"\"\n    sys.settrace(None)\n"
  },
  {
    "path": "gunicorn/dirty/__init__.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nDirty Arbiters - Separate process pool for long-running operations.\n\nDirty Arbiters provide a separate process pool for executing long-running,\nblocking operations (AI model loading, heavy computation) without blocking\nHTTP workers. Inspired by Erlang's dirty schedulers.\n\nKey Properties:\n- Completely separate from HTTP workers - can be killed/restarted independently\n- Stateful - loaded resources persist in dirty worker memory\n- Message-passing IPC via Unix sockets with JSON serialization\n- Explicit execute() API (no hidden IPC)\n- Asyncio-based for clean concurrent handling and future streaming support\n\"\"\"\n\nfrom .errors import (\n    DirtyError,\n    DirtyTimeoutError,\n    DirtyConnectionError,\n    DirtyWorkerError,\n    DirtyAppError,\n    DirtyAppNotFoundError,\n    DirtyProtocolError,\n)\n\nfrom .app import DirtyApp\n\nfrom .client import (\n    DirtyClient,\n    get_dirty_client,\n    get_dirty_client_async,\n    set_dirty_socket_path,\n    close_dirty_client,\n    close_dirty_client_async,\n)\n\n# Stash (shared state between workers)\nfrom . import stash\nfrom .stash import (\n    StashClient,\n    StashTable,\n    StashError,\n    StashTableNotFoundError,\n    StashKeyNotFoundError,\n)\n\n# Internal imports used by gunicorn core (not part of public API)\nfrom .arbiter import DirtyArbiter\n\n__all__ = [\n    # Errors\n    \"DirtyError\",\n    \"DirtyTimeoutError\",\n    \"DirtyConnectionError\",\n    \"DirtyWorkerError\",\n    \"DirtyAppError\",\n    \"DirtyAppNotFoundError\",\n    \"DirtyProtocolError\",\n    # App base class\n    \"DirtyApp\",\n    # Client\n    \"DirtyClient\",\n    \"get_dirty_client\",\n    \"get_dirty_client_async\",\n    \"close_dirty_client\",\n    \"close_dirty_client_async\",\n    # Stash (shared state)\n    \"stash\",\n    \"StashClient\",\n    \"StashTable\",\n    \"StashError\",\n    \"StashTableNotFoundError\",\n    \"StashKeyNotFoundError\",\n    # Internal (used by gunicorn core)\n    \"DirtyArbiter\",\n    \"set_dirty_socket_path\",\n]\n"
  },
  {
    "path": "gunicorn/dirty/app.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nDirty Application Base Class\n\nProvides the DirtyApp base class that all dirty applications must inherit from,\nand utilities for loading dirty apps from import paths.\n\"\"\"\n\nimport importlib\nimport sys\n\nfrom .errors import DirtyAppError, DirtyAppNotFoundError\n\n\nclass DirtyApp:\n    \"\"\"\n    Base class for dirty applications.\n\n    Dirty applications are loaded once when the dirty worker starts and\n    persist in memory for the lifetime of the worker. They are designed\n    for stateful resources like ML models, connection pools, etc.\n\n    Lifecycle\n    ---------\n    1. ``__init__()``: Called when the app is instantiated (once per worker)\n    2. ``init()``: Called after instantiation to initialize resources\n    3. ``__call__()``: Called for each request from HTTP workers\n    4. ``close()``: Called when the worker shuts down\n\n    State Persistence\n    -----------------\n    Instance variables persist across requests. This is the key feature\n    that enables loading heavy resources once and reusing them::\n\n        class MLApp(DirtyApp):\n            def init(self):\n                self.model = load_model()  # Loaded once, reused forever\n\n            def predict(self, data):\n                return self.model.predict(data)  # Same model for all requests\n\n    Thread Safety\n    -------------\n    With ``dirty_threads=1`` (default): Only one request runs at a time,\n    so no thread safety concerns.\n\n    With ``dirty_threads > 1``: Multiple requests may run concurrently\n    in the same worker. Your app MUST be thread-safe. Options:\n\n    - Use locks: ``threading.Lock()`` for shared state\n    - Use thread-local: ``threading.local()`` for per-thread state\n    - Use read-only state: Load models once in init(), never mutate\n\n    Example::\n\n        import threading\n\n        class ThreadSafeMLApp(DirtyApp):\n            def __init__(self):\n                self.models = {}\n                self._lock = threading.Lock()\n\n            def init(self):\n                self.models['default'] = load_model('base-model')\n\n            def load_model(self, name):\n                with self._lock:\n                    if name not in self.models:\n                        self.models[name] = load_model(name)\n                return {\"loaded\": True, \"name\": name}\n\n    Worker Allocation\n    -----------------\n    By default, all dirty workers load all apps. For apps that consume\n    significant memory (like large ML models), you can limit how many\n    workers load the app by setting the ``workers`` class attribute::\n\n        class HeavyModelApp(DirtyApp):\n            workers = 2  # Only 2 workers will load this app\n\n            def init(self):\n                self.model = load_10gb_model()\n\n    Subclasses should implement:\n        - init(): Called once at worker startup to initialize resources\n        - __call__(action, *args, **kwargs): Handle requests from HTTP workers\n        - close(): Called at worker shutdown to cleanup resources\n    \"\"\"\n\n    # Number of workers that should load this app.\n    # None means all workers (default, backward compatible).\n    # Set to an integer to limit how many workers load this app.\n    workers = None\n\n    def init(self):\n        \"\"\"\n        Initialize the application.\n\n        Called once when the dirty worker starts, after the app instance\n        is created. Use this for expensive initialization like loading\n        ML models, establishing database connections, etc.\n\n        This method is called in the child process after fork, so it's\n        safe to initialize non-fork-safe resources here.\n        \"\"\"\n\n    def __call__(self, action, *args, **kwargs):\n        \"\"\"\n        Handle a request from an HTTP worker.\n\n        Args:\n            action: The action/method name to execute\n            *args: Positional arguments for the action\n            **kwargs: Keyword arguments for the action\n\n        Returns:\n            The result of the action (must be JSON-serializable)\n\n        Raises:\n            ValueError: If the action is unknown\n            Any exception: Will be caught and returned as DirtyAppError\n        \"\"\"\n        method = getattr(self, action, None)\n        if method is None or action.startswith('_'):\n            raise ValueError(f\"Unknown action: {action}\")\n        return method(*args, **kwargs)\n\n    def close(self):\n        \"\"\"\n        Cleanup resources.\n\n        Called when the dirty worker is shutting down. Use this to\n        release resources like database connections, unload models, etc.\n        \"\"\"\n\n\ndef parse_dirty_app_spec(spec):\n    \"\"\"\n    Parse a dirty app specification.\n\n    Supports two formats:\n    - ``\"module:Class\"`` - standard format, all workers load the app\n    - ``\"module:Class:N\"`` - worker-limited format, only N workers load the app\n\n    Args:\n        spec: The app specification string\n\n    Returns:\n        tuple: (import_path, worker_count)\n            - import_path: The \"module:Class\" part for importing\n            - worker_count: Integer limit or None for all workers\n\n    Raises:\n        DirtyAppError: If the spec format is invalid or worker_count is < 1\n\n    Examples::\n\n        >>> parse_dirty_app_spec(\"myapp:App\")\n        (\"myapp:App\", None)\n\n        >>> parse_dirty_app_spec(\"myapp:App:2\")\n        (\"myapp:App\", 2)\n\n        >>> parse_dirty_app_spec(\"myapp.sub:App:1\")\n        (\"myapp.sub:App\", 1)\n    \"\"\"\n    if ':' not in spec:\n        raise DirtyAppError(\n            f\"Invalid import path format: {spec}. \"\n            f\"Expected 'module.path:ClassName' or 'module.path:ClassName:N'\",\n            app_path=spec\n        )\n\n    parts = spec.split(':')\n\n    # Standard format: \"module:Class\" or \"module.sub:Class\"\n    if len(parts) == 2:\n        return (spec, None)\n\n    # Worker-limited format: \"module:Class:N\"\n    if len(parts) == 3:\n        module_path, class_name, count_str = parts\n        import_path = f\"{module_path}:{class_name}\"\n\n        # Validate the worker count\n        try:\n            worker_count = int(count_str)\n        except ValueError:\n            raise DirtyAppError(\n                f\"Invalid worker count in spec: {spec}. \"\n                f\"Expected integer, got '{count_str}'\",\n                app_path=spec\n            )\n\n        if worker_count < 1:\n            raise DirtyAppError(\n                f\"Invalid worker count in spec: {spec}. \"\n                f\"Worker count must be >= 1, got {worker_count}\",\n                app_path=spec\n            )\n\n        return (import_path, worker_count)\n\n    # Too many colons\n    raise DirtyAppError(\n        f\"Invalid import path format: {spec}. \"\n        f\"Expected 'module.path:ClassName' or 'module.path:ClassName:N'\",\n        app_path=spec\n    )\n\n\ndef load_dirty_app(import_path):\n    \"\"\"\n    Load a dirty app class from an import path.\n\n    Args:\n        import_path: String in format 'module.path:ClassName'\n\n    Returns:\n        An instance of the dirty app class\n\n    Raises:\n        DirtyAppNotFoundError: If the module or class cannot be found\n        DirtyAppError: If the class is not a valid DirtyApp subclass\n    \"\"\"\n    if ':' not in import_path:\n        raise DirtyAppError(\n            f\"Invalid import path format: {import_path}. \"\n            f\"Expected 'module.path:ClassName'\",\n            app_path=import_path\n        )\n\n    module_path, class_name = import_path.rsplit(':', 1)\n\n    try:\n        # Import the module\n        if module_path in sys.modules:\n            module = sys.modules[module_path]\n        else:\n            module = importlib.import_module(module_path)\n    except ImportError as e:\n        raise DirtyAppNotFoundError(import_path) from e\n\n    # Get the class from the module\n    try:\n        app_class = getattr(module, class_name)\n    except AttributeError:\n        raise DirtyAppNotFoundError(import_path) from None\n\n    # Validate it's a class\n    if not isinstance(app_class, type):\n        raise DirtyAppError(\n            f\"{import_path} is not a class\",\n            app_path=import_path\n        )\n\n    # Create an instance\n    try:\n        app = app_class()\n    except Exception as e:\n        raise DirtyAppError(\n            f\"Failed to instantiate {import_path}: {e}\",\n            app_path=import_path\n        ) from e\n\n    # Validate it has the required methods\n    required_methods = ['init', '__call__', 'close']\n    for method_name in required_methods:\n        if not hasattr(app, method_name) or not callable(getattr(app, method_name)):\n            raise DirtyAppError(\n                f\"{import_path} is missing required method: {method_name}\",\n                app_path=import_path\n            )\n\n    return app\n\n\ndef load_dirty_apps(import_paths):\n    \"\"\"\n    Load multiple dirty apps from a list of import paths.\n\n    Args:\n        import_paths: List of import path strings\n\n    Returns:\n        dict: Mapping of import path to app instance\n\n    Raises:\n        DirtyAppError: If any app fails to load\n    \"\"\"\n    apps = {}\n    for import_path in import_paths:\n        apps[import_path] = load_dirty_app(import_path)\n    return apps\n\n\ndef get_app_workers_attribute(import_path):\n    \"\"\"\n    Get the workers class attribute from a dirty app without instantiating it.\n\n    This is used by the arbiter to determine how many workers should load\n    an app based on the class attribute, without needing to actually load\n    the app.\n\n    Args:\n        import_path: String in format 'module.path:ClassName'\n\n    Returns:\n        The workers class attribute value (int or None)\n\n    Raises:\n        DirtyAppNotFoundError: If the module or class cannot be found\n        DirtyAppError: If the import path format is invalid\n    \"\"\"\n    if ':' not in import_path:\n        raise DirtyAppError(\n            f\"Invalid import path format: {import_path}. \"\n            f\"Expected 'module.path:ClassName'\",\n            app_path=import_path\n        )\n\n    module_path, class_name = import_path.rsplit(':', 1)\n\n    try:\n        # Import the module\n        if module_path in sys.modules:\n            module = sys.modules[module_path]\n        else:\n            module = importlib.import_module(module_path)\n    except ImportError as e:\n        raise DirtyAppNotFoundError(import_path) from e\n\n    # Get the class from the module\n    try:\n        app_class = getattr(module, class_name)\n    except AttributeError:\n        raise DirtyAppNotFoundError(import_path) from None\n\n    # Validate it's a class\n    if not isinstance(app_class, type):\n        raise DirtyAppError(\n            f\"{import_path} is not a class\",\n            app_path=import_path\n        )\n\n    # Return the workers attribute (defaults to None if not set)\n    return getattr(app_class, 'workers', None)\n"
  },
  {
    "path": "gunicorn/dirty/arbiter.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nDirty Arbiter Process\n\nAsyncio-based arbiter that manages the dirty worker pool and routes\nrequests from HTTP workers to available dirty workers.\n\"\"\"\n\nimport asyncio\nimport errno\nimport fnmatch\nimport os\nimport signal\nimport tempfile\nimport time\n\nfrom gunicorn import util\n\nfrom .app import get_app_workers_attribute, parse_dirty_app_spec\nfrom .errors import (\n    DirtyError,\n    DirtyNoWorkersAvailableError,\n    DirtyTimeoutError,\n    DirtyWorkerError,\n)\nfrom .protocol import (\n    DirtyProtocol,\n    make_error_response,\n    make_response,\n    STASH_OP_PUT,\n    STASH_OP_GET,\n    STASH_OP_DELETE,\n    STASH_OP_KEYS,\n    STASH_OP_CLEAR,\n    STASH_OP_INFO,\n    STASH_OP_ENSURE,\n    STASH_OP_DELETE_TABLE,\n    STASH_OP_TABLES,\n    STASH_OP_EXISTS,\n    MANAGE_OP_ADD,\n    MANAGE_OP_REMOVE,\n)\nfrom .worker import DirtyWorker\n\n\nclass DirtyArbiter:\n    \"\"\"\n    Dirty arbiter that manages the dirty worker pool.\n\n    The arbiter runs an asyncio event loop and handles:\n    - Spawning and managing dirty worker processes\n    - Accepting connections from HTTP workers\n    - Routing requests to available dirty workers\n    - Monitoring worker health via heartbeat\n    \"\"\"\n\n    SIGNALS = [getattr(signal, \"SIG%s\" % x) for x in\n               \"HUP QUIT INT TERM TTIN TTOU USR1 USR2 CHLD\".split()]\n\n    # Worker boot error code\n    WORKER_BOOT_ERROR = 3\n\n    def __init__(self, cfg, log, socket_path=None, pidfile=None):\n        \"\"\"\n        Initialize the dirty arbiter.\n\n        Args:\n            cfg: Gunicorn config\n            log: Logger\n            socket_path: Path to the arbiter's Unix socket\n            pidfile: Well-known PID file location for orphan detection\n        \"\"\"\n        self.cfg = cfg\n        self.log = log\n        self.pid = None\n        self.ppid = os.getpid()\n        self.pidfile = pidfile  # Well-known location for orphan detection\n\n        # Use a temp directory for sockets\n        self.tmpdir = tempfile.mkdtemp(prefix=\"gunicorn-dirty-\")\n        self.socket_path = socket_path or os.path.join(\n            self.tmpdir, \"arbiter.sock\"\n        )\n\n        self.workers = {}  # pid -> DirtyWorker\n        self.worker_sockets = {}  # pid -> socket_path\n        self.worker_connections = {}  # pid -> (reader, writer)\n        self.worker_queues = {}  # pid -> asyncio.Queue\n        self.worker_consumers = {}  # pid -> asyncio.Task\n        self._worker_rr_index = 0  # Round-robin index for worker selection\n        self.worker_age = 0\n        self.alive = True\n        self.num_workers = self.cfg.dirty_workers  # Dynamic count for TTIN/TTOU\n\n        self._server = None\n        self._loop = None\n        self._pending_requests = {}  # request_id -> Future\n\n        # Per-app worker allocation tracking\n        # Maps import_path -> {import_path, worker_count, original_spec}\n        self.app_specs = {}\n        # Maps import_path -> set of worker PIDs that have loaded the app\n        self.app_worker_map = {}\n        # Maps worker_pid -> list of import_paths loaded by this worker\n        self.worker_app_map = {}\n        # Per-app round-robin indices for routing\n        self._app_rr_indices = {}\n        # Queue of app lists from dead workers to respawn with same apps\n        self._pending_respawns = []\n\n        # Stash (shared state) - global tables stored in arbiter\n        # Maps table_name -> dict of data\n        self.stash_tables = {}\n\n        # Parse app specs on init\n        self._parse_app_specs()\n\n    def _parse_app_specs(self):\n        \"\"\"\n        Parse all app specifications from config.\n\n        Populates self.app_specs with parsed information about each app,\n        including the import path and worker count limits.\n\n        Worker count priority:\n        1. Config override (e.g., \"module:Class:2\") - highest priority\n        2. Class attribute (e.g., workers = 2 on the class)\n        3. None (all workers) - default\n        \"\"\"\n        for spec in self.cfg.dirty_apps:\n            import_path, worker_count = parse_dirty_app_spec(spec)\n\n            # If no config override, check class attribute\n            if worker_count is None:\n                try:\n                    worker_count = get_app_workers_attribute(import_path)\n                except Exception as e:\n                    # Log but don't fail - we'll discover the error when loading\n                    self.log.warning(\n                        \"Could not read workers attribute from %s: %s\",\n                        import_path, e\n                    )\n\n            self.app_specs[import_path] = {\n                'import_path': import_path,\n                'worker_count': worker_count,\n                'original_spec': spec,\n            }\n            # Initialize the app_worker_map for this app\n            self.app_worker_map[import_path] = set()\n\n    def _get_minimum_workers(self):\n        \"\"\"\n        Calculate minimum number of workers required by app specs.\n\n        Returns the maximum worker_count across all apps that have limits.\n        Apps with worker_count=None don't impose a minimum.\n\n        Returns:\n            int: Minimum workers required (at least 1)\n        \"\"\"\n        min_required = 1\n        for spec in self.app_specs.values():\n            worker_count = spec['worker_count']\n            if worker_count is not None:\n                min_required = max(min_required, worker_count)\n        return min_required\n\n    def _get_apps_for_new_worker(self):\n        \"\"\"\n        Determine which apps a new worker should load.\n\n        Returns a list of import paths for apps that need more workers.\n        Apps with workers=None (all workers) are always included.\n        Apps with worker limits are included only if they haven't\n        reached their limit yet.\n\n        Returns:\n            List of import paths to load, or empty list if no apps need workers\n        \"\"\"\n        app_paths = []\n\n        for import_path, spec in self.app_specs.items():\n            worker_count = spec['worker_count']\n            current_workers = len(self.app_worker_map.get(import_path, set()))\n\n            # None means all workers should load this app\n            if worker_count is None:\n                app_paths.append(import_path)\n            # Otherwise check if we've reached the limit\n            elif current_workers < worker_count:\n                app_paths.append(import_path)\n\n        return app_paths\n\n    def _register_worker_apps(self, worker_pid, app_paths):\n        \"\"\"\n        Register which apps a worker has loaded.\n\n        Updates both app_worker_map and worker_app_map to track the\n        bidirectional relationship between workers and apps.\n\n        Args:\n            worker_pid: The PID of the worker\n            app_paths: List of app import paths loaded by this worker\n        \"\"\"\n        self.worker_app_map[worker_pid] = list(app_paths)\n\n        for app_path in app_paths:\n            if app_path not in self.app_worker_map:\n                self.app_worker_map[app_path] = set()\n            self.app_worker_map[app_path].add(worker_pid)\n\n    def _unregister_worker(self, worker_pid):\n        \"\"\"\n        Unregister a worker's apps when it exits.\n\n        Removes the worker from all tracking maps.\n\n        Args:\n            worker_pid: The PID of the worker to unregister\n        \"\"\"\n        # Get the apps this worker had\n        app_paths = self.worker_app_map.pop(worker_pid, [])\n\n        # Remove worker from each app's worker set\n        for app_path in app_paths:\n            if app_path in self.app_worker_map:\n                self.app_worker_map[app_path].discard(worker_pid)\n\n    def run(self):\n        \"\"\"Run the dirty arbiter (blocking call).\"\"\"\n        self.pid = os.getpid()\n        self.log.info(\"Dirty arbiter starting (pid: %s)\", self.pid)\n\n        # Write PID to well-known location for orphan detection\n        if self.pidfile:\n            try:\n                with open(self.pidfile, 'w') as f:\n                    f.write(str(self.pid))\n            except IOError as e:\n                self.log.warning(\"Failed to write PID file: %s\", e)\n\n        # Set socket path env var for dirty workers (enables stash access)\n        os.environ['GUNICORN_DIRTY_SOCKET'] = self.socket_path\n\n        # Call hook\n        self.cfg.on_dirty_starting(self)\n\n        # Set up signal handlers\n        self.init_signals()\n\n        # Set process title\n        util._setproctitle(\"dirty-arbiter\")\n\n        try:\n            asyncio.run(self._run_async())\n        except KeyboardInterrupt:\n            pass\n        finally:\n            self._cleanup_sync()\n\n    def init_signals(self):\n        \"\"\"Set up signal handlers.\"\"\"\n        for sig in self.SIGNALS:\n            signal.signal(sig, signal.SIG_DFL)\n\n        signal.signal(signal.SIGTERM, self._signal_handler)\n        signal.signal(signal.SIGQUIT, self._signal_handler)\n        signal.signal(signal.SIGINT, self._signal_handler)\n        signal.signal(signal.SIGHUP, self._signal_handler)\n        signal.signal(signal.SIGUSR1, self._signal_handler)\n        signal.signal(signal.SIGCHLD, self._signal_handler)\n        signal.signal(signal.SIGTTIN, self._signal_handler)\n        signal.signal(signal.SIGTTOU, self._signal_handler)\n\n    def _signal_handler(self, sig, frame):\n        \"\"\"Handle signals.\"\"\"\n        if sig == signal.SIGCHLD:\n            # Child exited - will be handled in reap_workers\n            if self._loop:\n                self._loop.call_soon_threadsafe(\n                    lambda: asyncio.create_task(self._handle_sigchld())\n                )\n            return\n\n        if sig == signal.SIGUSR1:\n            # Reopen log files\n            self.log.reopen_files()\n            return\n\n        if sig == signal.SIGHUP:\n            # Reload workers\n            if self._loop:\n                self._loop.call_soon_threadsafe(\n                    lambda: asyncio.create_task(self.reload())\n                )\n            return\n\n        if sig == signal.SIGTTIN:\n            # Increase number of workers\n            self.num_workers += 1\n            self.log.info(\"SIGTTIN: Increasing dirty workers to %s\",\n                          self.num_workers)\n            if self._loop:\n                self._loop.call_soon_threadsafe(\n                    lambda: asyncio.create_task(self.manage_workers())\n                )\n            return\n\n        if sig == signal.SIGTTOU:\n            # Decrease number of workers (respecting minimum)\n            min_workers = self._get_minimum_workers()\n            if self.num_workers <= min_workers:\n                self.log.warning(\n                    \"SIGTTOU: Cannot decrease below %s workers \"\n                    \"(required by app specs)\",\n                    min_workers\n                )\n                return\n            self.num_workers -= 1\n            self.log.info(\"SIGTTOU: Decreasing dirty workers to %s\",\n                          self.num_workers)\n            if self._loop:\n                self._loop.call_soon_threadsafe(\n                    lambda: asyncio.create_task(self.manage_workers())\n                )\n            return\n\n        # Shutdown signals\n        self.alive = False\n        if self._loop:\n            self._loop.call_soon_threadsafe(self._shutdown)\n\n    def _shutdown(self):\n        \"\"\"Initiate async shutdown.\"\"\"\n        if self._server:\n            self._server.close()\n\n    async def _run_async(self):\n        \"\"\"Main async loop - start server, manage workers.\"\"\"\n        self._loop = asyncio.get_running_loop()\n\n        # Remove socket if it exists\n        if os.path.exists(self.socket_path):\n            os.unlink(self.socket_path)\n\n        # Start Unix socket server for HTTP workers\n        self._server = await asyncio.start_unix_server(\n            self.handle_client,\n            path=self.socket_path\n        )\n\n        # Make socket accessible\n        os.chmod(self.socket_path, 0o600)\n\n        self.log.info(\"Dirty arbiter listening on %s\", self.socket_path)\n\n        # Spawn initial workers\n        await self.manage_workers()\n\n        # Start periodic tasks\n        monitor_task = asyncio.create_task(self._worker_monitor())\n\n        try:\n            async with self._server:\n                await self._server.serve_forever()\n        except (asyncio.CancelledError, RuntimeError):\n            # RuntimeError raised when server.close() is called during serve_forever()\n            pass\n        finally:\n            monitor_task.cancel()\n            try:\n                await monitor_task\n            except asyncio.CancelledError:\n                pass\n\n            await self.stop()\n\n    async def _worker_monitor(self):\n        \"\"\"Periodically check worker health and manage pool.\"\"\"\n        while self.alive:\n            await asyncio.sleep(1.0)\n\n            # Check if parent (main arbiter) died unexpectedly\n            if os.getppid() != self.ppid:\n                self.log.warning(\"Parent changed, shutting down dirty arbiter\")\n                self.alive = False\n                self._shutdown()\n                return\n\n            await self.murder_workers()\n            await self.manage_workers()\n\n    async def _handle_sigchld(self):\n        \"\"\"Handle SIGCHLD - reap dead workers.\"\"\"\n        self.reap_workers()\n        # Only spawn new workers if we're still alive\n        if self.alive:\n            await self.manage_workers()\n\n    async def handle_client(self, reader, writer):\n        \"\"\"\n        Handle a connection from an HTTP worker.\n\n        Routes requests to available dirty workers and returns responses.\n        Supports both regular responses and streaming (chunk-based) responses.\n        Also handles stash (shared state) operations.\n        \"\"\"\n        self.log.debug(\"New client connection from HTTP worker\")\n\n        try:\n            while self.alive:\n                try:\n                    message = await DirtyProtocol.read_message_async(reader)\n                except asyncio.IncompleteReadError:\n                    break\n\n                msg_type = message.get(\"type\")\n\n                # Handle stash operations\n                if msg_type == DirtyProtocol.MSG_TYPE_STASH:\n                    await self.handle_stash_request(message, writer)\n                # Handle status queries\n                elif msg_type == DirtyProtocol.MSG_TYPE_STATUS:\n                    await self.handle_status_request(message, writer)\n                # Handle worker management (add/remove workers)\n                elif msg_type == DirtyProtocol.MSG_TYPE_MANAGE:\n                    await self.handle_manage_request(message, writer)\n                else:\n                    # Route request to a dirty worker - pass writer for streaming\n                    await self.route_request(message, writer)\n        except Exception as e:\n            self.log.error(\"Client connection error: %s\", e)\n        finally:\n            writer.close()\n            try:\n                await writer.wait_closed()\n            except Exception:\n                pass\n\n    async def route_request(self, request, client_writer):\n        \"\"\"\n        Route a request to an available dirty worker via queue.\n\n        Each worker has a dedicated queue and consumer task. Requests are\n        submitted to the queue and processed sequentially by the consumer.\n\n        For streaming responses, messages (chunks) are forwarded directly\n        to the client_writer as they arrive from the worker.\n\n        Args:\n            request: Request message dict\n            client_writer: StreamWriter to send responses to client\n        \"\"\"\n        request_id = request.get(\"id\", \"unknown\")\n        app_path = request.get(\"app_path\")\n\n        # Find an available worker (filtered by app if specified)\n        worker_pid = await self._get_available_worker(app_path)\n        if worker_pid is None:\n            # Distinguish between no workers at all vs. no workers for this app\n            if not self.workers:\n                error = DirtyError(\"No dirty workers available\")\n            elif app_path and self.app_specs:\n                # Per-app allocation is configured and no workers have this app\n                error = DirtyNoWorkersAvailableError(app_path)\n            else:\n                error = DirtyError(\"No dirty workers available\")\n            response = make_error_response(request_id, error)\n            await DirtyProtocol.write_message_async(client_writer, response)\n            return\n\n        # Get queue (start consumer if needed)\n        if worker_pid not in self.worker_queues:\n            await self._start_worker_consumer(worker_pid)\n\n        queue = self.worker_queues[worker_pid]\n        future = asyncio.get_running_loop().create_future()\n\n        # Submit request to queue with client writer for streaming support\n        await queue.put((request, client_writer, future))\n\n        # Wait for completion (streaming messages forwarded by consumer)\n        try:\n            await future\n        except Exception as e:\n            response = make_error_response(\n                request_id,\n                DirtyWorkerError(f\"Request failed: {e}\", worker_id=worker_pid)\n            )\n            await DirtyProtocol.write_message_async(client_writer, response)\n\n    async def _start_worker_consumer(self, worker_pid):\n        \"\"\"Start a consumer task for a worker's request queue.\"\"\"\n        queue = asyncio.Queue()\n        self.worker_queues[worker_pid] = queue\n\n        async def consumer():\n            while self.alive:\n                try:\n                    request, client_writer, future = await queue.get()\n                    try:\n                        await self._execute_on_worker(\n                            worker_pid, request, client_writer\n                        )\n                        if not future.done():\n                            future.set_result(None)\n                    except Exception as e:\n                        if not future.done():\n                            future.set_exception(e)\n                    finally:\n                        queue.task_done()\n                except asyncio.CancelledError:\n                    break\n\n        task = asyncio.create_task(consumer())\n        self.worker_consumers[worker_pid] = task\n\n    async def _execute_on_worker(self, worker_pid, request, client_writer):\n        \"\"\"\n        Execute request on a specific worker (called by consumer).\n\n        Handles both regular responses and streaming (chunk-based) responses.\n        For streaming, chunk and end messages are forwarded directly to the\n        client_writer as they arrive from the worker.\n        \"\"\"\n        request_id = request.get(\"id\", \"unknown\")\n\n        try:\n            reader, writer = await self._get_worker_connection(worker_pid)\n            await DirtyProtocol.write_message_async(writer, request)\n\n            # Read messages until we get a response, end, or error\n            while True:\n                try:\n                    message = await asyncio.wait_for(\n                        DirtyProtocol.read_message_async(reader),\n                        timeout=self.cfg.dirty_timeout\n                    )\n                except asyncio.TimeoutError:\n                    response = make_error_response(\n                        request_id,\n                        DirtyTimeoutError(\"Worker timeout\", self.cfg.dirty_timeout)\n                    )\n                    await DirtyProtocol.write_message_async(client_writer, response)\n                    return\n\n                msg_type = message.get(\"type\")\n\n                # Forward chunk messages to client\n                if msg_type == DirtyProtocol.MSG_TYPE_CHUNK:\n                    await DirtyProtocol.write_message_async(client_writer, message)\n                    continue\n\n                # Forward end message and complete\n                if msg_type == DirtyProtocol.MSG_TYPE_END:\n                    await DirtyProtocol.write_message_async(client_writer, message)\n                    return\n\n                # Forward response or error and complete\n                if msg_type in (DirtyProtocol.MSG_TYPE_RESPONSE,\n                                DirtyProtocol.MSG_TYPE_ERROR):\n                    await DirtyProtocol.write_message_async(client_writer, message)\n                    return\n\n                # Unknown message type - log and continue\n                self.log.warning(\"Unknown message type from worker: %s\", msg_type)\n\n        except Exception as e:\n            self.log.error(\"Error executing on worker %s: %s\", worker_pid, e)\n            self._close_worker_connection(worker_pid)\n            response = make_error_response(\n                request_id,\n                DirtyWorkerError(f\"Worker communication failed: {e}\",\n                                 worker_id=worker_pid)\n            )\n            await DirtyProtocol.write_message_async(client_writer, response)\n\n    async def _get_available_worker(self, app_path=None):\n        \"\"\"\n        Get an available worker PID using round-robin selection.\n\n        If app_path is provided, only returns workers that have loaded\n        that specific app. Uses per-app round-robin to ensure fair\n        distribution among eligible workers.\n\n        Args:\n            app_path: Optional import path of the target app. If None,\n                     returns any worker using global round-robin.\n\n        Returns:\n            Worker PID or None if no eligible workers are available.\n        \"\"\"\n        # Determine eligible workers\n        if app_path and self.app_specs:\n            # Per-app allocation is configured - must return a worker\n            # that has this specific app\n            if app_path in self.app_worker_map:\n                eligible_pids = list(self.app_worker_map[app_path])\n            else:\n                # App not known or no workers have it\n                return None\n        else:\n            # No specific app requested, or no app specs configured\n            # (backward compatible) - any worker will do\n            eligible_pids = list(self.workers.keys())\n\n        if not eligible_pids:\n            return None\n\n        # Per-app round-robin for fairness\n        if app_path and self.app_specs:\n            idx = self._app_rr_indices.get(app_path, 0)\n            self._app_rr_indices[app_path] = (idx + 1) % len(eligible_pids)\n        else:\n            idx = self._worker_rr_index\n            self._worker_rr_index = (idx + 1) % len(eligible_pids)\n\n        return eligible_pids[idx % len(eligible_pids)]\n\n    async def _get_worker_connection(self, worker_pid):\n        \"\"\"Get or create connection to a worker.\"\"\"\n        if worker_pid in self.worker_connections:\n            return self.worker_connections[worker_pid]\n\n        socket_path = self.worker_sockets.get(worker_pid)\n        if not socket_path:\n            raise DirtyError(f\"No socket for worker {worker_pid}\")\n\n        # Wait for socket to be available\n        for _ in range(50):  # 5 seconds max\n            if os.path.exists(socket_path):\n                break\n            await asyncio.sleep(0.1)\n        else:\n            raise DirtyError(f\"Worker socket not ready: {socket_path}\")\n\n        reader, writer = await asyncio.open_unix_connection(socket_path)\n        self.worker_connections[worker_pid] = (reader, writer)\n        return reader, writer\n\n    def _close_worker_connection(self, worker_pid):\n        \"\"\"Close connection to a worker.\"\"\"\n        if worker_pid in self.worker_connections:\n            _reader, writer = self.worker_connections.pop(worker_pid)\n            writer.close()\n\n    # -------------------------------------------------------------------------\n    # Stash (shared state) operations - handled directly in arbiter\n    # -------------------------------------------------------------------------\n\n    async def handle_status_request(self, message, client_writer):\n        \"\"\"\n        Handle a status query request.\n\n        Returns information about the dirty arbiter and its workers.\n\n        Args:\n            message: Status request message\n            client_writer: StreamWriter to send response to client\n        \"\"\"\n        request_id = message.get(\"id\", \"unknown\")\n        now = time.monotonic()\n\n        workers_info = []\n        for pid, worker in self.workers.items():\n            try:\n                last_update = worker.tmp.last_update()\n                last_heartbeat = round(now - last_update, 2)\n            except (OSError, ValueError, AttributeError):\n                last_heartbeat = None\n\n            workers_info.append({\n                \"pid\": pid,\n                \"age\": worker.age,\n                \"apps\": getattr(worker, 'app_paths', []),\n                \"booted\": getattr(worker, 'booted', False),\n                \"last_heartbeat\": last_heartbeat,\n            })\n\n        workers_info.sort(key=lambda w: w[\"age\"])\n\n        result = {\n            \"arbiter_pid\": self.pid,\n            \"workers\": workers_info,\n            \"worker_count\": len(workers_info),\n            \"apps\": list(self.app_specs.keys()) if self.app_specs else [],\n        }\n\n        response = make_response(request_id, result)\n        await DirtyProtocol.write_message_async(client_writer, response)\n\n    async def handle_manage_request(self, message, client_writer):\n        \"\"\"\n        Handle a worker management request.\n\n        Supports adding or removing dirty workers via protocol messages.\n\n        Args:\n            message: Manage request message\n            client_writer: StreamWriter to send response to client\n        \"\"\"\n        request_id = message.get(\"id\", \"unknown\")\n        op = message.get(\"op\")\n        count = max(1, int(message.get(\"count\", 1)))\n\n        try:\n            if op == MANAGE_OP_ADD:\n                # Add workers - only loads apps that need more workers\n                spawned = 0\n                for _ in range(count):\n                    result = self.spawn_worker()\n                    if result is not None:\n                        self.num_workers += 1\n                        spawned += 1\n                    await asyncio.sleep(0.1)\n\n                # Provide feedback about why no workers were spawned\n                if spawned == 0:\n                    result = {\n                        \"success\": True,\n                        \"operation\": \"add\",\n                        \"requested\": count,\n                        \"spawned\": 0,\n                        \"reason\": \"All apps have reached their worker limits\",\n                        \"total_workers\": len(self.workers),\n                        \"target_workers\": self.num_workers,\n                    }\n                else:\n                    result = {\n                        \"success\": True,\n                        \"operation\": \"add\",\n                        \"requested\": count,\n                        \"spawned\": spawned,\n                        \"total_workers\": len(self.workers),\n                        \"target_workers\": self.num_workers,\n                    }\n\n            elif op == MANAGE_OP_REMOVE:\n                # Remove workers (similar to TTOU signal but via message)\n                min_workers = self._get_minimum_workers()\n                removed = 0\n\n                for _ in range(count):\n                    if self.num_workers <= min_workers:\n                        break\n                    if len(self.workers) <= 1:\n                        break\n\n                    self.num_workers -= 1\n\n                    # Kill oldest worker\n                    oldest_pid = min(self.workers.keys(),\n                                     key=lambda p: self.workers[p].age)\n                    self.kill_worker(oldest_pid, signal.SIGTERM)\n                    removed += 1\n                    await asyncio.sleep(0.1)\n\n                result = {\n                    \"success\": True,\n                    \"operation\": \"remove\",\n                    \"requested\": count,\n                    \"removed\": removed,\n                    \"total_workers\": len(self.workers),\n                    \"target_workers\": self.num_workers,\n                }\n\n            else:\n                error = DirtyError(f\"Unknown manage operation: {op}\")\n                response = make_error_response(request_id, error)\n                await DirtyProtocol.write_message_async(client_writer, response)\n                return\n\n            self.log.info(\"Worker management: %s %d workers (spawned/removed: %d)\",\n                          \"add\" if op == MANAGE_OP_ADD else \"remove\",\n                          count,\n                          result.get(\"spawned\", result.get(\"removed\", 0)))\n\n            response = make_response(request_id, result)\n            await DirtyProtocol.write_message_async(client_writer, response)\n\n        except Exception as e:\n            self.log.error(\"Manage operation error: %s\", e)\n            response = make_error_response(request_id, DirtyError(str(e)))\n            await DirtyProtocol.write_message_async(client_writer, response)\n\n    async def handle_stash_request(self, message, client_writer):\n        \"\"\"\n        Handle a stash operation directly in the arbiter.\n\n        All stash tables are stored in arbiter memory for simplicity\n        and fast access.\n\n        Args:\n            message: Stash operation message\n            client_writer: StreamWriter to send response to client\n        \"\"\"\n        request_id = message.get(\"id\", \"unknown\")\n        op = message.get(\"op\")\n        table = message.get(\"table\", \"\")\n        key = message.get(\"key\")\n        value = message.get(\"value\")\n        pattern = message.get(\"pattern\")\n\n        try:\n            result = None\n\n            if op == STASH_OP_PUT:\n                # Auto-create table if needed\n                if table not in self.stash_tables:\n                    self.stash_tables[table] = {}\n                self.stash_tables[table][key] = value\n                result = True\n\n            elif op == STASH_OP_GET:\n                if table not in self.stash_tables:\n                    result = {\"error\": \"key_not_found\"}\n                elif key not in self.stash_tables[table]:\n                    result = {\"error\": \"key_not_found\"}\n                else:\n                    result = self.stash_tables[table][key]\n\n            elif op == STASH_OP_DELETE:\n                if table in self.stash_tables and key in self.stash_tables[table]:\n                    del self.stash_tables[table][key]\n                    result = True\n                else:\n                    result = False\n\n            elif op == STASH_OP_KEYS:\n                if table not in self.stash_tables:\n                    result = []\n                else:\n                    all_keys = list(self.stash_tables[table].keys())\n                    if pattern:\n                        all_keys = [k for k in all_keys\n                                    if fnmatch.fnmatch(str(k), pattern)]\n                    result = all_keys\n\n            elif op == STASH_OP_CLEAR:\n                if table in self.stash_tables:\n                    self.stash_tables[table].clear()\n                result = True\n\n            elif op == STASH_OP_INFO:\n                if table not in self.stash_tables:\n                    result = {\"error\": \"table_not_found\"}\n                else:\n                    result = {\n                        \"size\": len(self.stash_tables[table]),\n                        \"table\": table,\n                    }\n\n            elif op == STASH_OP_ENSURE:\n                if table not in self.stash_tables:\n                    self.stash_tables[table] = {}\n                result = True\n\n            elif op == STASH_OP_DELETE_TABLE:\n                if table in self.stash_tables:\n                    del self.stash_tables[table]\n                    result = True\n                else:\n                    result = False\n\n            elif op == STASH_OP_TABLES:\n                result = list(self.stash_tables.keys())\n\n            elif op == STASH_OP_EXISTS:\n                if table not in self.stash_tables:\n                    result = False\n                elif key is None:\n                    result = True\n                else:\n                    result = key in self.stash_tables[table]\n\n            else:\n                error = DirtyError(f\"Unknown stash operation: {op}\")\n                response = make_error_response(request_id, error)\n                await DirtyProtocol.write_message_async(client_writer, response)\n                return\n\n            # Handle error results\n            if isinstance(result, dict) and \"error\" in result:\n                error_type = result[\"error\"]\n                if error_type == \"table_not_found\":\n                    error = DirtyError(f\"Table not found: {table}\")\n                elif error_type == \"key_not_found\":\n                    error = DirtyError(f\"Key not found: {key}\")\n                else:\n                    error = DirtyError(str(result))\n                error.error_type = f\"Stash{error_type.title().replace('_', '')}Error\"\n                response = make_error_response(request_id, error)\n            else:\n                response = make_response(request_id, result)\n\n            await DirtyProtocol.write_message_async(client_writer, response)\n\n        except Exception as e:\n            self.log.error(\"Stash operation error: %s\", e)\n            response = make_error_response(request_id, DirtyError(str(e)))\n            await DirtyProtocol.write_message_async(client_writer, response)\n\n    async def manage_workers(self):\n        \"\"\"Maintain the number of dirty workers.\"\"\"\n        if not self.alive:\n            return\n\n        num_workers = self.num_workers\n\n        # Spawn workers if needed\n        while self.alive and len(self.workers) < num_workers:\n            result = self.spawn_worker()\n            if result is None:\n                # No apps need more workers - stop spawning\n                break\n            await asyncio.sleep(0.1)\n\n        # Kill excess workers\n        while len(self.workers) > num_workers:\n            # Kill oldest worker\n            oldest_pid = min(self.workers.keys(),\n                             key=lambda p: self.workers[p].age)\n            self.kill_worker(oldest_pid, signal.SIGTERM)\n            await asyncio.sleep(0.1)\n\n    def spawn_worker(self, force_all_apps=False):\n        \"\"\"\n        Spawn a new dirty worker.\n\n        Worker app assignment follows these priorities:\n        1. If there are pending respawns (from dead workers), use those apps\n        2. Otherwise, determine apps for a new worker based on allocation\n        3. If force_all_apps=True, spawn with all apps regardless of limits\n\n        Args:\n            force_all_apps: If True, spawn worker with all apps ignoring limits\n\n        Returns:\n            Worker PID in parent process, or None if no apps need workers\n        \"\"\"\n        # Priority 1: Respawn dead worker with same apps\n        if self._pending_respawns:\n            app_paths = self._pending_respawns.pop(0)\n        elif force_all_apps:\n            # Force spawn with all apps (used by TTIN signal)\n            app_paths = list(self.app_specs.keys())\n        else:\n            # Priority 2: New worker for initial pool\n            app_paths = self._get_apps_for_new_worker()\n\n        if not app_paths:\n            self.log.debug(\"No apps need more workers, skipping spawn\")\n            return None\n\n        self.worker_age += 1\n        socket_path = os.path.join(\n            self.tmpdir, f\"worker-{self.worker_age}.sock\"\n        )\n\n        worker = DirtyWorker(\n            age=self.worker_age,\n            ppid=self.pid,\n            app_paths=app_paths,  # Only assigned apps, not all apps\n            cfg=self.cfg,\n            log=self.log,\n            socket_path=socket_path\n        )\n\n        pid = os.fork()\n        if pid != 0:\n            # Parent process\n            worker.pid = pid\n            self.workers[pid] = worker\n            self.worker_sockets[pid] = socket_path\n\n            # Register which apps this worker has\n            self._register_worker_apps(pid, app_paths)\n\n            self.cfg.dirty_post_fork(self, worker)\n            self.log.info(\"Spawned dirty worker (pid: %s) with apps: %s\",\n                          pid, app_paths)\n            return pid\n\n        # Child process - use os._exit() to avoid asyncio cleanup issues\n        worker.pid = os.getpid()\n        try:\n            util._setproctitle(f\"dirty-worker [{self.cfg.proc_name}]\")\n            worker.init_process()\n            os._exit(0)\n        except SystemExit as e:\n            os._exit(e.code if e.code is not None else 0)\n        except Exception:\n            self.log.exception(\"Exception in dirty worker process\")\n            if not worker.booted:\n                os._exit(self.WORKER_BOOT_ERROR)\n            os._exit(1)\n\n    def kill_worker(self, pid, sig):\n        \"\"\"Kill a worker by PID.\"\"\"\n        try:\n            os.kill(pid, sig)\n        except OSError as e:\n            if e.errno == errno.ESRCH:\n                self._cleanup_worker(pid)\n\n    def _cleanup_worker(self, pid):\n        \"\"\"\n        Clean up after a worker exits.\n\n        Saves the dead worker's app list to pending respawns so the\n        replacement worker gets the same apps.\n        \"\"\"\n        self._close_worker_connection(pid)\n\n        # Cancel consumer task\n        if pid in self.worker_consumers:\n            self.worker_consumers[pid].cancel()\n            del self.worker_consumers[pid]\n\n        # Remove queue\n        self.worker_queues.pop(pid, None)\n\n        # Save dead worker's apps for respawn BEFORE unregistering\n        if pid in self.worker_app_map:\n            dead_apps = list(self.worker_app_map[pid])\n            if dead_apps:\n                self._pending_respawns.append(dead_apps)\n\n        # Now safe to unregister the worker's apps\n        self._unregister_worker(pid)\n\n        worker = self.workers.pop(pid, None)\n        if worker:\n            self.cfg.dirty_worker_exit(self, worker)\n        socket_path = self.worker_sockets.pop(pid, None)\n        if socket_path and os.path.exists(socket_path):\n            try:\n                os.unlink(socket_path)\n            except OSError:\n                pass\n\n    async def murder_workers(self):\n        \"\"\"Kill workers that have timed out.\"\"\"\n        if not self.cfg.dirty_timeout:\n            return\n\n        for pid, worker in list(self.workers.items()):\n            try:\n                if time.monotonic() - worker.tmp.last_update() <= self.cfg.dirty_timeout:\n                    continue\n            except (OSError, ValueError):\n                continue\n\n            if not worker.aborted:\n                self.log.critical(\"DIRTY WORKER TIMEOUT (pid:%s)\", pid)\n                worker.aborted = True\n                self.kill_worker(pid, signal.SIGABRT)\n            else:\n                self.kill_worker(pid, signal.SIGKILL)\n\n    def reap_workers(self):\n        \"\"\"Reap dead worker processes.\"\"\"\n        try:\n            while True:\n                wpid, status = os.waitpid(-1, os.WNOHANG)\n                if not wpid:\n                    break\n\n                exitcode = None\n                if os.WIFEXITED(status):\n                    exitcode = os.WEXITSTATUS(status)\n                elif os.WIFSIGNALED(status):\n                    sig = os.WTERMSIG(status)\n                    self.log.warning(\"Dirty worker (pid:%s) killed by signal %s\",\n                                     wpid, sig)\n\n                if exitcode == self.WORKER_BOOT_ERROR:\n                    self.log.error(\"Dirty worker failed to boot (pid:%s)\", wpid)\n\n                self._cleanup_worker(wpid)\n                self.log.info(\"Dirty worker exited (pid:%s)\", wpid)\n        except OSError as e:\n            if e.errno != errno.ECHILD:\n                raise\n\n    async def reload(self):\n        \"\"\"Reload workers (SIGHUP handling).\"\"\"\n        self.log.info(\"Reloading dirty workers\")\n\n        # Spawn new workers\n        for _ in range(self.cfg.dirty_workers):\n            self.spawn_worker()\n            await asyncio.sleep(0.1)\n\n        # Kill old workers\n        old_workers = list(self.workers.keys())\n        for pid in old_workers[self.cfg.dirty_workers:]:\n            self.kill_worker(pid, signal.SIGTERM)\n\n    async def stop(self, graceful=True):\n        \"\"\"Stop all workers.\"\"\"\n        # Cancel all consumer tasks\n        for task in self.worker_consumers.values():\n            task.cancel()\n\n        sig = signal.SIGTERM if graceful else signal.SIGQUIT\n        limit = time.time() + self.cfg.dirty_graceful_timeout\n\n        # Signal all workers\n        for pid in list(self.workers.keys()):\n            self.kill_worker(pid, sig)\n\n        # Wait for workers to exit\n        while self.workers and time.time() < limit:\n            self.reap_workers()\n            await asyncio.sleep(0.1)\n\n        # Force kill remaining workers\n        for pid in list(self.workers.keys()):\n            self.kill_worker(pid, signal.SIGKILL)\n        self.reap_workers()\n\n    def _cleanup_sync(self):\n        \"\"\"Synchronous cleanup on exit.\"\"\"\n        # Remove PID file\n        if self.pidfile and os.path.exists(self.pidfile):\n            try:\n                os.unlink(self.pidfile)\n            except OSError:\n                pass\n\n        # Clean up socket\n        if os.path.exists(self.socket_path):\n            try:\n                os.unlink(self.socket_path)\n            except OSError:\n                pass\n\n        # Clean up temp directory\n        try:\n            for f in os.listdir(self.tmpdir):\n                os.unlink(os.path.join(self.tmpdir, f))\n            os.rmdir(self.tmpdir)\n        except OSError:\n            pass\n\n        self.log.info(\"Dirty arbiter exiting (pid: %s)\", self.pid)\n"
  },
  {
    "path": "gunicorn/dirty/client.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nDirty Client\n\nClient for HTTP workers to communicate with the dirty worker pool.\nProvides both sync and async APIs.\n\"\"\"\n\nimport asyncio\nimport contextvars\nimport os\nimport socket\nimport threading\nimport time\nimport uuid\n\nfrom .errors import (\n    DirtyConnectionError,\n    DirtyError,\n    DirtyTimeoutError,\n)\nfrom .protocol import (\n    DirtyProtocol,\n    make_request,\n)\n\n\nclass DirtyClient:\n    \"\"\"\n    Client for calling dirty workers from HTTP workers.\n\n    Provides both sync and async APIs. The sync API is for traditional\n    sync workers (sync, gthread), while the async API is for async\n    workers (asgi, gevent, eventlet).\n    \"\"\"\n\n    def __init__(self, socket_path, timeout=30.0):\n        \"\"\"\n        Initialize the dirty client.\n\n        Args:\n            socket_path: Path to the dirty arbiter's Unix socket\n            timeout: Default timeout for operations in seconds\n        \"\"\"\n        self.socket_path = socket_path\n        self.timeout = timeout\n        self._sock = None\n        self._reader = None\n        self._writer = None\n        self._lock = threading.Lock()\n\n    # -------------------------------------------------------------------------\n    # Sync API (for sync HTTP workers)\n    # -------------------------------------------------------------------------\n\n    def connect(self):\n        \"\"\"\n        Establish sync socket connection to arbiter.\n\n        Raises:\n            DirtyConnectionError: If connection fails\n        \"\"\"\n        if self._sock is not None:\n            return\n\n        try:\n            self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)\n            self._sock.settimeout(self.timeout)\n            self._sock.connect(self.socket_path)\n        except (socket.error, OSError) as e:\n            self._sock = None\n            raise DirtyConnectionError(\n                f\"Failed to connect to dirty arbiter: {e}\",\n                socket_path=self.socket_path\n            ) from e\n\n    def execute(self, app_path, action, *args, **kwargs):\n        \"\"\"\n        Execute an action on a dirty app (sync/blocking).\n\n        Args:\n            app_path: Import path of the dirty app (e.g., 'myapp.ml:MLApp')\n            action: Action to call on the app\n            *args: Positional arguments\n            **kwargs: Keyword arguments\n\n        Returns:\n            Result from the dirty app action\n\n        Raises:\n            DirtyConnectionError: If connection fails\n            DirtyTimeoutError: If operation times out\n            DirtyError: If execution fails\n        \"\"\"\n        with self._lock:\n            return self._execute_locked(app_path, action, args, kwargs)\n\n    def _execute_locked(self, app_path, action, args, kwargs):\n        \"\"\"Execute while holding the lock.\"\"\"\n        # Ensure connected\n        if self._sock is None:\n            self.connect()\n\n        # Build request\n        request_id = str(uuid.uuid4())\n        request = make_request(\n            request_id=request_id,\n            app_path=app_path,\n            action=action,\n            args=args,\n            kwargs=kwargs\n        )\n\n        try:\n            # Send request\n            DirtyProtocol.write_message(self._sock, request)\n\n            # Receive response\n            response = DirtyProtocol.read_message(self._sock)\n\n            # Handle response\n            return self._handle_response(response)\n        except socket.timeout:\n            self._close_socket()\n            raise DirtyTimeoutError(\n                \"Timeout waiting for dirty app response\",\n                timeout=self.timeout\n            )\n        except Exception as e:\n            self._close_socket()\n            if isinstance(e, DirtyError):\n                raise\n            raise DirtyConnectionError(f\"Communication error: {e}\") from e\n\n    def stream(self, app_path, action, *args, **kwargs):\n        \"\"\"\n        Stream results from a dirty app action (sync).\n\n        This method returns an iterator that yields chunks from a streaming\n        response. Use this for actions that return generators.\n\n        Args:\n            app_path: Import path of the dirty app (e.g., 'myapp.ml:MLApp')\n            action: Action to call on the app\n            *args: Positional arguments\n            **kwargs: Keyword arguments\n\n        Yields:\n            Chunks of data from the streaming response\n\n        Raises:\n            DirtyConnectionError: If connection fails\n            DirtyTimeoutError: If operation times out\n            DirtyError: If execution fails\n\n        Example::\n\n            for chunk in client.stream(\"myapp.llm:LLMApp\", \"generate\", prompt):\n                print(chunk, end=\"\", flush=True)\n        \"\"\"\n        return DirtyStreamIterator(self, app_path, action, args, kwargs)\n\n    def _handle_response(self, response):\n        \"\"\"Handle response message, extracting result or raising error.\"\"\"\n        msg_type = response.get(\"type\")\n\n        if msg_type == DirtyProtocol.MSG_TYPE_RESPONSE:\n            return response.get(\"result\")\n        elif msg_type == DirtyProtocol.MSG_TYPE_ERROR:\n            error_info = response.get(\"error\", {})\n            error = DirtyError.from_dict(error_info)\n            raise error\n        else:\n            raise DirtyError(f\"Unknown response type: {msg_type}\")\n\n    def _close_socket(self):\n        \"\"\"Close the socket connection.\"\"\"\n        if self._sock is not None:\n            try:\n                self._sock.close()\n            except Exception:\n                pass\n            self._sock = None\n\n    def close(self):\n        \"\"\"Close the sync connection.\"\"\"\n        with self._lock:\n            self._close_socket()\n\n    # -------------------------------------------------------------------------\n    # Async API (for async HTTP workers)\n    # -------------------------------------------------------------------------\n\n    async def connect_async(self):\n        \"\"\"\n        Establish async connection to arbiter.\n\n        Raises:\n            DirtyConnectionError: If connection fails\n        \"\"\"\n        if self._writer is not None:\n            return\n\n        try:\n            self._reader, self._writer = await asyncio.wait_for(\n                asyncio.open_unix_connection(self.socket_path),\n                timeout=self.timeout\n            )\n        except asyncio.TimeoutError:\n            raise DirtyTimeoutError(\n                \"Timeout connecting to dirty arbiter\",\n                timeout=self.timeout\n            )\n        except (OSError, ConnectionError) as e:\n            raise DirtyConnectionError(\n                f\"Failed to connect to dirty arbiter: {e}\",\n                socket_path=self.socket_path\n            ) from e\n\n    async def execute_async(self, app_path, action, *args, **kwargs):\n        \"\"\"\n        Execute an action on a dirty app (async/non-blocking).\n\n        Args:\n            app_path: Import path of the dirty app\n            action: Action to call on the app\n            *args: Positional arguments\n            **kwargs: Keyword arguments\n\n        Returns:\n            Result from the dirty app action\n\n        Raises:\n            DirtyConnectionError: If connection fails\n            DirtyTimeoutError: If operation times out\n            DirtyError: If execution fails\n        \"\"\"\n        # Ensure connected\n        if self._writer is None:\n            await self.connect_async()\n\n        # Build request\n        request_id = str(uuid.uuid4())\n        request = make_request(\n            request_id=request_id,\n            app_path=app_path,\n            action=action,\n            args=args,\n            kwargs=kwargs\n        )\n\n        try:\n            # Send request\n            await DirtyProtocol.write_message_async(self._writer, request)\n\n            # Receive response with timeout\n            response = await asyncio.wait_for(\n                DirtyProtocol.read_message_async(self._reader),\n                timeout=self.timeout\n            )\n\n            # Handle response\n            return self._handle_response(response)\n        except asyncio.TimeoutError:\n            await self._close_async()\n            raise DirtyTimeoutError(\n                \"Timeout waiting for dirty app response\",\n                timeout=self.timeout\n            )\n        except Exception as e:\n            await self._close_async()\n            if isinstance(e, DirtyError):\n                raise\n            raise DirtyConnectionError(f\"Communication error: {e}\") from e\n\n    def stream_async(self, app_path, action, *args, **kwargs):\n        \"\"\"\n        Stream results from a dirty app action (async).\n\n        This method returns an async iterator that yields chunks from a\n        streaming response. Use this for actions that return generators.\n\n        Args:\n            app_path: Import path of the dirty app (e.g., 'myapp.ml:MLApp')\n            action: Action to call on the app\n            *args: Positional arguments\n            **kwargs: Keyword arguments\n\n        Yields:\n            Chunks of data from the streaming response\n\n        Raises:\n            DirtyConnectionError: If connection fails\n            DirtyTimeoutError: If operation times out\n            DirtyError: If execution fails\n\n        Example::\n\n            async for chunk in client.stream_async(\"myapp.llm:LLMApp\", \"generate\", prompt):\n                await response.write(chunk)\n        \"\"\"\n        return DirtyAsyncStreamIterator(self, app_path, action, args, kwargs)\n\n    async def _close_async(self):\n        \"\"\"Close the async connection.\"\"\"\n        if self._writer is not None:\n            try:\n                self._writer.close()\n                await self._writer.wait_closed()\n            except Exception:\n                pass\n            self._writer = None\n            self._reader = None\n\n    async def close_async(self):\n        \"\"\"Close the async connection.\"\"\"\n        await self._close_async()\n\n    # -------------------------------------------------------------------------\n    # Context managers\n    # -------------------------------------------------------------------------\n\n    def __enter__(self):\n        self.connect()\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        self.close()\n\n    async def __aenter__(self):\n        await self.connect_async()\n        return self\n\n    async def __aexit__(self, exc_type, exc_val, exc_tb):\n        await self.close_async()\n\n\n# =============================================================================\n# Stream Iterator classes\n# =============================================================================\n\n\nclass DirtyStreamIterator:\n    \"\"\"\n    Iterator for streaming responses from dirty workers (sync).\n\n    This class is returned by `DirtyClient.stream()` and yields chunks\n    from a streaming response until the end message is received.\n\n    Uses a deadline-based timeout approach:\n    - Total stream timeout: limits entire stream duration\n    - Idle timeout: limits gap between chunks (defaults to total timeout)\n    \"\"\"\n\n    # Default idle timeout between chunks (seconds)\n    DEFAULT_IDLE_TIMEOUT = 30.0\n\n    # Threshold for applying per-read timeout (seconds)\n    # When remaining time is above this, use a larger timeout for efficiency\n    _TIMEOUT_THRESHOLD = 5.0\n\n    def __init__(self, client, app_path, action, args, kwargs,\n                 idle_timeout=None):\n        self.client = client\n        self.app_path = app_path\n        self.action = action\n        self.args = args\n        self.kwargs = kwargs\n        self._started = False\n        self._exhausted = False\n        self._request_id = None\n        self._deadline = None\n        self._last_chunk_time = None\n        # Idle timeout: max time between chunks\n        self._idle_timeout = (\n            idle_timeout if idle_timeout is not None\n            else min(self.DEFAULT_IDLE_TIMEOUT, client.timeout)\n        )\n\n    def __iter__(self):\n        return self\n\n    def __next__(self):\n        if self._exhausted:\n            raise StopIteration\n\n        if not self._started:\n            self._start_request()\n            self._started = True\n\n        return self._read_next_chunk()\n\n    def _start_request(self):\n        \"\"\"Send the initial request to the arbiter.\"\"\"\n        with self.client._lock:\n            if self.client._sock is None:\n                self.client.connect()\n\n            # Set deadline for entire stream\n            now = time.monotonic()\n            self._deadline = now + self.client.timeout\n            self._last_chunk_time = now\n\n            self._request_id = str(uuid.uuid4())\n            request = make_request(\n                self._request_id,\n                self.app_path,\n                self.action,\n                args=self.args,\n                kwargs=self.kwargs,\n            )\n            DirtyProtocol.write_message(self.client._sock, request)\n\n    def _read_next_chunk(self):\n        \"\"\"Read the next message from the stream.\"\"\"\n        with self.client._lock:\n            # Check total stream deadline\n            now = time.monotonic()\n            if now >= self._deadline:\n                self._exhausted = True\n                raise DirtyTimeoutError(\n                    \"Stream exceeded total timeout\",\n                    timeout=self.client.timeout\n                )\n\n            remaining = self._deadline - now\n\n            # Set socket timeout based on remaining time\n            # Fast path: use larger timeout when plenty of time remains\n            if remaining > self._TIMEOUT_THRESHOLD:\n                read_timeout = self._TIMEOUT_THRESHOLD\n            else:\n                read_timeout = min(remaining, self._idle_timeout)\n\n            try:\n                self.client._sock.settimeout(read_timeout)\n                response = DirtyProtocol.read_message(self.client._sock)\n            except socket.timeout:\n                # Check which timeout was hit\n                now = time.monotonic()\n                if now >= self._deadline:\n                    self._exhausted = True\n                    raise DirtyTimeoutError(\n                        \"Stream exceeded total timeout\",\n                        timeout=self.client.timeout\n                    )\n                idle_duration = now - self._last_chunk_time\n                self._exhausted = True\n                raise DirtyTimeoutError(\n                    f\"Timeout waiting for next chunk (idle {idle_duration:.1f}s)\",\n                    timeout=self._idle_timeout\n                )\n            except Exception as e:\n                self._exhausted = True\n                self.client._close_socket()\n                raise DirtyConnectionError(f\"Communication error: {e}\") from e\n\n            # Update last chunk time for idle tracking\n            self._last_chunk_time = time.monotonic()\n\n            msg_type = response.get(\"type\")\n\n            # Chunk message - return the data\n            if msg_type == DirtyProtocol.MSG_TYPE_CHUNK:\n                return response.get(\"data\")\n\n            # End message - stop iteration\n            if msg_type == DirtyProtocol.MSG_TYPE_END:\n                self._exhausted = True\n                raise StopIteration\n\n            # Error message - raise exception\n            if msg_type == DirtyProtocol.MSG_TYPE_ERROR:\n                self._exhausted = True\n                error_info = response.get(\"error\", {})\n                raise DirtyError.from_dict(error_info)\n\n            # Regular response - shouldn't happen for streaming, but handle it\n            if msg_type == DirtyProtocol.MSG_TYPE_RESPONSE:\n                self._exhausted = True\n                # Return the result as the only chunk then stop\n                raise StopIteration\n\n            # Unknown type\n            self._exhausted = True\n            raise DirtyError(f\"Unknown message type: {msg_type}\")\n\n\nclass DirtyAsyncStreamIterator:\n    \"\"\"\n    Async iterator for streaming responses from dirty workers.\n\n    This class is returned by `DirtyClient.stream_async()` and yields chunks\n    from a streaming response until the end message is received.\n\n    Uses a deadline-based timeout approach for efficiency:\n    - Total stream timeout: limits entire stream duration\n    - Idle timeout: limits gap between chunks (defaults to total timeout)\n\n    This avoids the overhead of asyncio.wait_for() on every chunk read.\n    \"\"\"\n\n    # Default idle timeout between chunks (seconds)\n    DEFAULT_IDLE_TIMEOUT = 30.0\n\n    def __init__(self, client, app_path, action, args, kwargs,\n                 idle_timeout=None):\n        self.client = client\n        self.app_path = app_path\n        self.action = action\n        self.args = args\n        self.kwargs = kwargs\n        self._started = False\n        self._exhausted = False\n        self._request_id = None\n        self._deadline = None\n        self._last_chunk_time = None\n        # Idle timeout: max time between chunks\n        self._idle_timeout = (\n            idle_timeout if idle_timeout is not None\n            else min(self.DEFAULT_IDLE_TIMEOUT, client.timeout)\n        )\n\n    def __aiter__(self):\n        return self\n\n    async def __anext__(self):\n        if self._exhausted:\n            raise StopAsyncIteration\n\n        if not self._started:\n            await self._start_request()\n            self._started = True\n\n        return await self._read_next_chunk()\n\n    async def _start_request(self):\n        \"\"\"Send the initial request to the arbiter.\"\"\"\n        if self.client._writer is None:\n            await self.client.connect_async()\n\n        # Set deadline for entire stream\n        now = time.monotonic()\n        self._deadline = now + self.client.timeout\n        self._last_chunk_time = now\n\n        self._request_id = str(uuid.uuid4())\n        request = make_request(\n            self._request_id,\n            self.app_path,\n            self.action,\n            args=self.args,\n            kwargs=self.kwargs,\n        )\n        await DirtyProtocol.write_message_async(self.client._writer, request)\n\n    # Threshold for applying timeout wrapper (seconds)\n    # When remaining time is above this, skip timeout for performance\n    _TIMEOUT_THRESHOLD = 5.0\n\n    async def _read_next_chunk(self):\n        \"\"\"Read the next message from the stream.\"\"\"\n        # Calculate remaining time until deadline\n        now = time.monotonic()\n\n        # Check total stream deadline\n        if now >= self._deadline:\n            self._exhausted = True\n            raise DirtyTimeoutError(\n                \"Stream exceeded total timeout\",\n                timeout=self.client.timeout\n            )\n\n        remaining = self._deadline - now\n\n        try:\n            # Fast path: skip timeout wrapper when we have plenty of time\n            # This avoids asyncio.wait_for() overhead for most chunks\n            if remaining > self._TIMEOUT_THRESHOLD:\n                response = await DirtyProtocol.read_message_async(\n                    self.client._reader\n                )\n            else:\n                # Near deadline: apply timeout protection\n                read_timeout = min(remaining, self._idle_timeout)\n                response = await asyncio.wait_for(\n                    DirtyProtocol.read_message_async(self.client._reader),\n                    timeout=read_timeout\n                )\n        except asyncio.TimeoutError:\n            self._exhausted = True\n            now = time.monotonic()\n            if now >= self._deadline:\n                raise DirtyTimeoutError(\n                    \"Stream exceeded total timeout\",\n                    timeout=self.client.timeout\n                )\n            idle_duration = now - self._last_chunk_time\n            raise DirtyTimeoutError(\n                f\"Timeout waiting for next chunk (idle {idle_duration:.1f}s)\",\n                timeout=self._idle_timeout\n            )\n        except Exception as e:\n            self._exhausted = True\n            await self.client._close_async()\n            raise DirtyConnectionError(f\"Communication error: {e}\") from e\n\n        # Update last chunk time for idle tracking\n        self._last_chunk_time = time.monotonic()\n\n        msg_type = response.get(\"type\")\n\n        # Chunk message - return the data\n        if msg_type == DirtyProtocol.MSG_TYPE_CHUNK:\n            return response.get(\"data\")\n\n        # End message - stop iteration\n        if msg_type == DirtyProtocol.MSG_TYPE_END:\n            self._exhausted = True\n            raise StopAsyncIteration\n\n        # Error message - raise exception\n        if msg_type == DirtyProtocol.MSG_TYPE_ERROR:\n            self._exhausted = True\n            error_info = response.get(\"error\", {})\n            raise DirtyError.from_dict(error_info)\n\n        # Regular response - shouldn't happen for streaming\n        if msg_type == DirtyProtocol.MSG_TYPE_RESPONSE:\n            self._exhausted = True\n            raise StopAsyncIteration\n\n        # Unknown type\n        self._exhausted = True\n        raise DirtyError(f\"Unknown message type: {msg_type}\")\n\n\n# =============================================================================\n# Thread-local and context-local client management\n# =============================================================================\n\n# Thread-local storage for sync workers\n_thread_local = threading.local()\n\n# Context var for async workers\n_async_client_var: contextvars.ContextVar[DirtyClient] = contextvars.ContextVar(\n    'dirty_client'\n)\n\n# Global socket path (set by arbiter)\n_dirty_socket_path = None\n\n\ndef set_dirty_socket_path(path):\n    \"\"\"Set the global dirty socket path (called during initialization).\"\"\"\n    global _dirty_socket_path  # pylint: disable=global-statement\n    _dirty_socket_path = path\n\n    # Also set the stash socket path (uses same arbiter socket)\n    from .stash import set_stash_socket_path\n    set_stash_socket_path(path)\n\n\ndef get_dirty_socket_path():\n    \"\"\"Get the dirty socket path.\"\"\"\n    if _dirty_socket_path is None:\n        # Check environment variable\n        path = os.environ.get('GUNICORN_DIRTY_SOCKET')\n        if path:\n            return path\n        raise DirtyError(\n            \"Dirty socket path not configured. \"\n            \"Make sure dirty_workers > 0 and dirty_apps are configured.\"\n        )\n    return _dirty_socket_path\n\n\ndef get_dirty_client(timeout=30.0) -> DirtyClient:\n    \"\"\"\n    Get or create a thread-local sync client.\n\n    This is the recommended way to get a client in sync HTTP workers.\n\n    Args:\n        timeout: Timeout for operations in seconds\n\n    Returns:\n        DirtyClient: Thread-local client instance\n\n    Example::\n\n        from gunicorn.dirty import get_dirty_client\n\n        def my_view(request):\n            client = get_dirty_client()\n            result = client.execute(\"myapp.ml:MLApp\", \"inference\", data)\n            return result\n    \"\"\"\n    client = getattr(_thread_local, 'dirty_client', None)\n    if client is None:\n        socket_path = get_dirty_socket_path()\n        client = DirtyClient(socket_path, timeout=timeout)\n        _thread_local.dirty_client = client\n    return client\n\n\nasync def get_dirty_client_async(timeout=30.0) -> DirtyClient:\n    \"\"\"\n    Get or create a context-local async client.\n\n    This is the recommended way to get a client in async HTTP workers.\n\n    Args:\n        timeout: Timeout for operations in seconds\n\n    Returns:\n        DirtyClient: Context-local client instance\n\n    Example::\n\n        from gunicorn.dirty import get_dirty_client_async\n\n        async def my_view(request):\n            client = await get_dirty_client_async()\n            result = await client.execute_async(\"myapp.ml:MLApp\", \"inference\", data)\n            return result\n    \"\"\"\n    try:\n        client = _async_client_var.get()\n    except LookupError:\n        socket_path = get_dirty_socket_path()\n        client = DirtyClient(socket_path, timeout=timeout)\n        _async_client_var.set(client)\n    return client\n\n\ndef close_dirty_client():\n    \"\"\"Close the thread-local client (call on worker exit).\"\"\"\n    client = getattr(_thread_local, 'dirty_client', None)\n    if client is not None:\n        client.close()\n        _thread_local.dirty_client = None\n\n\nasync def close_dirty_client_async():\n    \"\"\"Close the context-local async client.\"\"\"\n    try:\n        client = _async_client_var.get()\n        await client.close_async()\n    except LookupError:\n        pass\n"
  },
  {
    "path": "gunicorn/dirty/errors.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nDirty Arbiters Error Classes\n\nException hierarchy for dirty worker pool operations.\n\"\"\"\n\n\nclass DirtyError(Exception):\n    \"\"\"Base exception for all dirty arbiter errors.\"\"\"\n\n    def __init__(self, message, details=None):\n        self.message = message\n        self.details = details or {}\n        super().__init__(message)\n\n    def __str__(self):\n        if self.details:\n            return f\"{self.message}: {self.details}\"\n        return self.message\n\n    def to_dict(self):\n        \"\"\"Serialize error for protocol transmission.\"\"\"\n        return {\n            \"error_type\": self.__class__.__name__,\n            \"message\": self.message,\n            \"details\": self.details,\n        }\n\n    @classmethod\n    def from_dict(cls, data):\n        \"\"\"Deserialize error from protocol transmission.\n\n        Creates an error instance from a serialized dict. The returned\n        error will be an instance of the appropriate subclass based on\n        the error_type field, but constructed using the base DirtyError\n        __init__ to preserve all details.\n        \"\"\"\n        error_classes = {\n            \"DirtyError\": DirtyError,\n            \"DirtyTimeoutError\": DirtyTimeoutError,\n            \"DirtyConnectionError\": DirtyConnectionError,\n            \"DirtyWorkerError\": DirtyWorkerError,\n            \"DirtyAppError\": DirtyAppError,\n            \"DirtyAppNotFoundError\": DirtyAppNotFoundError,\n            \"DirtyNoWorkersAvailableError\": DirtyNoWorkersAvailableError,\n            \"DirtyProtocolError\": DirtyProtocolError,\n        }\n        error_type = data.get(\"error_type\", \"DirtyError\")\n        error_class = error_classes.get(error_type, DirtyError)\n\n        # Create instance and set attributes directly to bypass\n        # subclass __init__ complexity while preserving error type\n        error = Exception.__new__(error_class)\n        error.message = data.get(\"message\", \"Unknown error\")\n        error.details = data.get(\"details\") or {}\n        Exception.__init__(error, error.message)\n\n        # Set subclass-specific attributes from details\n        if error_class == DirtyTimeoutError:\n            error.timeout = error.details.get(\"timeout\")\n        elif error_class == DirtyConnectionError:\n            error.socket_path = error.details.get(\"socket_path\")\n        elif error_class == DirtyWorkerError:\n            error.worker_id = error.details.get(\"worker_id\")\n            error.traceback = error.details.get(\"traceback\")\n        elif error_class in (DirtyAppError, DirtyAppNotFoundError):\n            error.app_path = error.details.get(\"app_path\")\n            error.action = error.details.get(\"action\")\n            error.traceback = error.details.get(\"traceback\")\n        elif error_class == DirtyNoWorkersAvailableError:\n            error.app_path = error.details.get(\"app_path\")\n\n        return error\n\n\nclass DirtyTimeoutError(DirtyError):\n    \"\"\"Raised when a dirty operation times out.\"\"\"\n\n    def __init__(self, message=\"Operation timed out\", timeout=None):\n        details = {\"timeout\": timeout} if timeout else {}\n        super().__init__(message, details)\n        self.timeout = timeout\n\n\nclass DirtyConnectionError(DirtyError):\n    \"\"\"Raised when connection to dirty arbiter fails.\"\"\"\n\n    def __init__(self, message=\"Connection failed\", socket_path=None):\n        details = {\"socket_path\": socket_path} if socket_path else {}\n        super().__init__(message, details)\n        self.socket_path = socket_path\n\n\nclass DirtyWorkerError(DirtyError):\n    \"\"\"Raised when a dirty worker encounters an error.\"\"\"\n\n    def __init__(self, message, worker_id=None, traceback=None):\n        details = {}\n        if worker_id is not None:\n            details[\"worker_id\"] = worker_id\n        if traceback:\n            details[\"traceback\"] = traceback\n        super().__init__(message, details)\n        self.worker_id = worker_id\n        self.traceback = traceback\n\n\nclass DirtyAppError(DirtyError):\n    \"\"\"Raised when a dirty app encounters an error during execution.\"\"\"\n\n    def __init__(self, message, app_path=None, action=None, traceback=None):\n        details = {}\n        if app_path:\n            details[\"app_path\"] = app_path\n        if action:\n            details[\"action\"] = action\n        if traceback:\n            details[\"traceback\"] = traceback\n        super().__init__(message, details)\n        self.app_path = app_path\n        self.action = action\n        self.traceback = traceback\n\n\nclass DirtyAppNotFoundError(DirtyAppError):\n    \"\"\"Raised when a dirty app is not found.\"\"\"\n\n    def __init__(self, app_path):\n        super().__init__(f\"Dirty app not found: {app_path}\", app_path=app_path)\n\n\nclass DirtyNoWorkersAvailableError(DirtyError):\n    \"\"\"\n    Raised when no workers are available for the requested app.\n\n    This exception is raised when a request targets an app that has\n    worker limits configured, and no workers with that app are currently\n    available (e.g., all workers for that app crashed and haven't been\n    respawned yet).\n\n    Web applications can catch this exception to provide graceful\n    degradation, such as queuing requests for retry or showing a\n    maintenance page.\n\n    Example::\n\n        from gunicorn.dirty import get_dirty_client\n        from gunicorn.dirty.errors import DirtyNoWorkersAvailableError\n\n        def my_view(request):\n            client = get_dirty_client()\n            try:\n                result = client.execute(\"myapp.ml:HeavyModel\", \"predict\", data)\n            except DirtyNoWorkersAvailableError as e:\n                return {\"error\": \"Service temporarily unavailable\",\n                        \"app\": e.app_path}\n    \"\"\"\n\n    def __init__(self, app_path, message=None):\n        if message is None:\n            message = f\"No workers available for app: {app_path}\"\n        super().__init__(message, details={\"app_path\": app_path})\n        self.app_path = app_path\n\n\nclass DirtyProtocolError(DirtyError):\n    \"\"\"Raised when there is a protocol-level error.\"\"\"\n\n    def __init__(self, message=\"Protocol error\", raw_data=None):\n        details = {}\n        if raw_data is not None:\n            # Truncate raw data for safety\n            if isinstance(raw_data, bytes):\n                raw_data = raw_data[:100].hex()\n            details[\"raw_data\"] = str(raw_data)[:200]\n        super().__init__(message, details)\n"
  },
  {
    "path": "gunicorn/dirty/protocol.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nDirty Worker Binary Protocol\n\nBinary message framing over Unix sockets, inspired by OpenBSD msgctl/msgsnd.\nReplaces JSON protocol for efficient binary data transfer.\n\nHeader Format (16 bytes):\n+--------+--------+--------+--------+--------+--------+--------+--------+\n|  Magic (2B)     | Ver(1) | MType  |        Payload Length (4B)        |\n+--------+--------+--------+--------+--------+--------+--------+--------+\n|                       Request ID (8 bytes)                            |\n+--------+--------+--------+--------+--------+--------+--------+--------+\n\n- Magic: 0x47 0x44 (\"GD\" for Gunicorn Dirty)\n- Version: 0x01\n- MType: Message type (REQUEST, RESPONSE, ERROR, CHUNK, END)\n- Length: Payload size (big-endian uint32, max 64MB)\n- Request ID: uint64 (replaces UUID string)\n\nPayload is TLV-encoded (see tlv.py).\n\"\"\"\n\nimport asyncio\nimport socket\nimport struct\n\nfrom .errors import DirtyProtocolError\nfrom .tlv import TLVEncoder\n\n\n# Protocol constants\nMAGIC = b\"GD\"  # 0x47 0x44\nVERSION = 0x01\n\n# Message types (1 byte)\nMSG_TYPE_REQUEST = 0x01\nMSG_TYPE_RESPONSE = 0x02\nMSG_TYPE_ERROR = 0x03\nMSG_TYPE_CHUNK = 0x04\nMSG_TYPE_END = 0x05\nMSG_TYPE_STASH = 0x10  # Stash operations (shared state between workers)\nMSG_TYPE_STATUS = 0x11  # Status query for arbiter/workers\nMSG_TYPE_MANAGE = 0x12  # Worker management (add/remove workers)\n\n# Message type names (for backwards compatibility with old API)\nMSG_TYPE_REQUEST_STR = \"request\"\nMSG_TYPE_RESPONSE_STR = \"response\"\nMSG_TYPE_ERROR_STR = \"error\"\nMSG_TYPE_CHUNK_STR = \"chunk\"\nMSG_TYPE_END_STR = \"end\"\nMSG_TYPE_STASH_STR = \"stash\"\nMSG_TYPE_STATUS_STR = \"status\"\nMSG_TYPE_MANAGE_STR = \"manage\"\n\n# Map int types to string names\nMSG_TYPE_TO_STR = {\n    MSG_TYPE_REQUEST: MSG_TYPE_REQUEST_STR,\n    MSG_TYPE_RESPONSE: MSG_TYPE_RESPONSE_STR,\n    MSG_TYPE_ERROR: MSG_TYPE_ERROR_STR,\n    MSG_TYPE_CHUNK: MSG_TYPE_CHUNK_STR,\n    MSG_TYPE_END: MSG_TYPE_END_STR,\n    MSG_TYPE_STASH: MSG_TYPE_STASH_STR,\n    MSG_TYPE_STATUS: MSG_TYPE_STATUS_STR,\n    MSG_TYPE_MANAGE: MSG_TYPE_MANAGE_STR,\n}\n\n# Map string names to int types\nMSG_TYPE_FROM_STR = {v: k for k, v in MSG_TYPE_TO_STR.items()}\n\n# Stash operation codes\nSTASH_OP_PUT = 1\nSTASH_OP_GET = 2\nSTASH_OP_DELETE = 3\nSTASH_OP_KEYS = 4\nSTASH_OP_CLEAR = 5\nSTASH_OP_INFO = 6\nSTASH_OP_ENSURE = 7\nSTASH_OP_DELETE_TABLE = 8\nSTASH_OP_TABLES = 9\nSTASH_OP_EXISTS = 10\n\n# Manage operation codes\nMANAGE_OP_ADD = 1      # Add/spawn workers\nMANAGE_OP_REMOVE = 2   # Remove/kill workers\n\n# Header format: Magic (2) + Version (1) + Type (1) + Length (4) + RequestID (8) = 16\nHEADER_FORMAT = \">2sBBIQ\"\nHEADER_SIZE = struct.calcsize(HEADER_FORMAT)\n\n# Maximum message size (64 MB)\nMAX_MESSAGE_SIZE = 64 * 1024 * 1024\n\n\nclass BinaryProtocol:\n    \"\"\"Binary message protocol for dirty worker IPC.\"\"\"\n\n    # Export constants for external use\n    HEADER_SIZE = HEADER_SIZE\n    MAX_MESSAGE_SIZE = MAX_MESSAGE_SIZE\n\n    MSG_TYPE_REQUEST = MSG_TYPE_REQUEST_STR\n    MSG_TYPE_RESPONSE = MSG_TYPE_RESPONSE_STR\n    MSG_TYPE_ERROR = MSG_TYPE_ERROR_STR\n    MSG_TYPE_CHUNK = MSG_TYPE_CHUNK_STR\n    MSG_TYPE_END = MSG_TYPE_END_STR\n    MSG_TYPE_STASH = MSG_TYPE_STASH_STR\n    MSG_TYPE_STATUS = MSG_TYPE_STATUS_STR\n    MSG_TYPE_MANAGE = MSG_TYPE_MANAGE_STR\n\n    @staticmethod\n    def encode_header(msg_type: int, request_id: int, payload_length: int) -> bytes:\n        \"\"\"\n        Encode the 16-byte message header.\n\n        Args:\n            msg_type: Message type (MSG_TYPE_REQUEST, etc.)\n            request_id: Unique request identifier (uint64)\n            payload_length: Length of the TLV-encoded payload\n\n        Returns:\n            bytes: 16-byte header\n        \"\"\"\n        return struct.pack(HEADER_FORMAT, MAGIC, VERSION, msg_type,\n                           payload_length, request_id)\n\n    @staticmethod\n    def decode_header(data: bytes) -> tuple:\n        \"\"\"\n        Decode the 16-byte message header.\n\n        Args:\n            data: 16 bytes of header data\n\n        Returns:\n            tuple: (msg_type, request_id, payload_length)\n\n        Raises:\n            DirtyProtocolError: If header is invalid\n        \"\"\"\n        if len(data) < HEADER_SIZE:\n            raise DirtyProtocolError(\n                f\"Header too short: {len(data)} bytes, expected {HEADER_SIZE}\",\n                raw_data=data\n            )\n\n        magic, version, msg_type, length, request_id = struct.unpack(\n            HEADER_FORMAT, data[:HEADER_SIZE]\n        )\n\n        if magic != MAGIC:\n            raise DirtyProtocolError(\n                f\"Invalid magic: {magic!r}, expected {MAGIC!r}\",\n                raw_data=data[:20]\n            )\n\n        if version != VERSION:\n            raise DirtyProtocolError(\n                f\"Unsupported protocol version: {version}, expected {VERSION}\",\n                raw_data=data[:20]\n            )\n\n        if msg_type not in MSG_TYPE_TO_STR:\n            raise DirtyProtocolError(\n                f\"Unknown message type: 0x{msg_type:02x}\",\n                raw_data=data[:20]\n            )\n\n        if length > MAX_MESSAGE_SIZE:\n            raise DirtyProtocolError(\n                f\"Message too large: {length} bytes (max: {MAX_MESSAGE_SIZE})\"\n            )\n\n        return msg_type, request_id, length\n\n    @staticmethod\n    def encode_request(request_id: int, app_path: str, action: str,\n                       args: tuple = None, kwargs: dict = None) -> bytes:\n        \"\"\"\n        Encode a request message.\n\n        Args:\n            request_id: Unique request identifier (uint64)\n            app_path: Import path of the dirty app\n            action: Action to call on the app\n            args: Positional arguments\n            kwargs: Keyword arguments\n\n        Returns:\n            bytes: Complete message (header + payload)\n        \"\"\"\n        payload_dict = {\n            \"app_path\": app_path,\n            \"action\": action,\n            \"args\": list(args) if args else [],\n            \"kwargs\": kwargs or {},\n        }\n        payload = TLVEncoder.encode(payload_dict)\n        header = BinaryProtocol.encode_header(MSG_TYPE_REQUEST, request_id,\n                                              len(payload))\n        return header + payload\n\n    @staticmethod\n    def encode_response(request_id: int, result) -> bytes:\n        \"\"\"\n        Encode a success response message.\n\n        Args:\n            request_id: Request identifier this responds to\n            result: Result value (must be TLV-serializable)\n\n        Returns:\n            bytes: Complete message (header + payload)\n        \"\"\"\n        payload_dict = {\"result\": result}\n        payload = TLVEncoder.encode(payload_dict)\n        header = BinaryProtocol.encode_header(MSG_TYPE_RESPONSE, request_id,\n                                              len(payload))\n        return header + payload\n\n    @staticmethod\n    def encode_error(request_id: int, error) -> bytes:\n        \"\"\"\n        Encode an error response message.\n\n        Args:\n            request_id: Request identifier this responds to\n            error: DirtyError instance, dict, or Exception\n\n        Returns:\n            bytes: Complete message (header + payload)\n        \"\"\"\n        from .errors import DirtyError\n\n        if isinstance(error, DirtyError):\n            error_dict = error.to_dict()\n        elif isinstance(error, dict):\n            error_dict = error\n        else:\n            error_dict = {\n                \"error_type\": type(error).__name__,\n                \"message\": str(error),\n                \"details\": {},\n            }\n\n        payload_dict = {\"error\": error_dict}\n        payload = TLVEncoder.encode(payload_dict)\n        header = BinaryProtocol.encode_header(MSG_TYPE_ERROR, request_id,\n                                              len(payload))\n        return header + payload\n\n    @staticmethod\n    def encode_chunk(request_id: int, data) -> bytes:\n        \"\"\"\n        Encode a chunk message for streaming responses.\n\n        Args:\n            request_id: Request identifier this chunk belongs to\n            data: Chunk data (must be TLV-serializable)\n\n        Returns:\n            bytes: Complete message (header + payload)\n        \"\"\"\n        payload_dict = {\"data\": data}\n        payload = TLVEncoder.encode(payload_dict)\n        header = BinaryProtocol.encode_header(MSG_TYPE_CHUNK, request_id,\n                                              len(payload))\n        return header + payload\n\n    @staticmethod\n    def encode_end(request_id: int) -> bytes:\n        \"\"\"\n        Encode an end-of-stream message.\n\n        Args:\n            request_id: Request identifier this ends\n\n        Returns:\n            bytes: Complete message (header + empty payload)\n        \"\"\"\n        # End message has empty payload\n        header = BinaryProtocol.encode_header(MSG_TYPE_END, request_id, 0)\n        return header\n\n    @staticmethod\n    def encode_status(request_id: int) -> bytes:\n        \"\"\"\n        Encode a status query message.\n\n        Args:\n            request_id: Request identifier\n\n        Returns:\n            bytes: Complete message (header + empty payload)\n        \"\"\"\n        # Status query has empty payload\n        header = BinaryProtocol.encode_header(MSG_TYPE_STATUS, request_id, 0)\n        return header\n\n    @staticmethod\n    def encode_manage(request_id: int, op: int, count: int = 1) -> bytes:\n        \"\"\"\n        Encode a worker management message.\n\n        Args:\n            request_id: Request identifier\n            op: Management operation (MANAGE_OP_ADD or MANAGE_OP_REMOVE)\n            count: Number of workers to add/remove\n\n        Returns:\n            bytes: Complete message (header + payload)\n        \"\"\"\n        payload_dict = {\n            \"op\": op,\n            \"count\": count,\n        }\n        payload = TLVEncoder.encode(payload_dict)\n        header = BinaryProtocol.encode_header(MSG_TYPE_MANAGE, request_id,\n                                              len(payload))\n        return header + payload\n\n    @staticmethod\n    def encode_stash(request_id: int, op: int, table: str,\n                     key=None, value=None, pattern=None) -> bytes:\n        \"\"\"\n        Encode a stash operation message.\n\n        Args:\n            request_id: Unique request identifier (uint64)\n            op: Stash operation code (STASH_OP_*)\n            table: Table name\n            key: Optional key for put/get/delete operations\n            value: Optional value for put operation\n            pattern: Optional pattern for keys operation\n\n        Returns:\n            bytes: Complete message (header + payload)\n        \"\"\"\n        payload_dict = {\n            \"op\": op,\n            \"table\": table,\n        }\n        if key is not None:\n            payload_dict[\"key\"] = key\n        if value is not None:\n            payload_dict[\"value\"] = value\n        if pattern is not None:\n            payload_dict[\"pattern\"] = pattern\n\n        payload = TLVEncoder.encode(payload_dict)\n        header = BinaryProtocol.encode_header(MSG_TYPE_STASH, request_id,\n                                              len(payload))\n        return header + payload\n\n    @staticmethod\n    def decode_message(data: bytes) -> tuple:\n        \"\"\"\n        Decode a complete message (header + payload).\n\n        Args:\n            data: Complete message bytes\n\n        Returns:\n            tuple: (msg_type_str, request_id, payload_dict)\n                   msg_type_str is the string name (e.g., \"request\")\n                   payload_dict is the decoded TLV payload as a dict\n\n        Raises:\n            DirtyProtocolError: If message is malformed\n        \"\"\"\n        msg_type, request_id, length = BinaryProtocol.decode_header(data)\n\n        if len(data) < HEADER_SIZE + length:\n            raise DirtyProtocolError(\n                f\"Incomplete message: expected {HEADER_SIZE + length} bytes, \"\n                f\"got {len(data)}\",\n                raw_data=data[:50]\n            )\n\n        if length == 0:\n            # End message has empty payload\n            payload_dict = {}\n        else:\n            payload_data = data[HEADER_SIZE:HEADER_SIZE + length]\n            try:\n                payload_dict = TLVEncoder.decode_full(payload_data)\n            except DirtyProtocolError:\n                raise\n            except Exception as e:\n                raise DirtyProtocolError(\n                    f\"Failed to decode TLV payload: {e}\",\n                    raw_data=payload_data[:50]\n                )\n\n        # Convert to dict format similar to old JSON protocol\n        msg_type_str = MSG_TYPE_TO_STR[msg_type]\n\n        return msg_type_str, request_id, payload_dict\n\n    # -------------------------------------------------------------------------\n    # Async API (primary - for DirtyArbiter and DirtyWorker)\n    # -------------------------------------------------------------------------\n\n    @staticmethod\n    async def read_message_async(reader: asyncio.StreamReader) -> dict:\n        \"\"\"\n        Read a complete binary message from async stream.\n\n        Args:\n            reader: asyncio StreamReader\n\n        Returns:\n            dict: Message dict with 'type', 'id', and payload fields\n\n        Raises:\n            DirtyProtocolError: If read fails or message is malformed\n            asyncio.IncompleteReadError: If connection closed mid-read\n        \"\"\"\n        # Read header\n        try:\n            header = await reader.readexactly(HEADER_SIZE)\n        except asyncio.IncompleteReadError as e:\n            if len(e.partial) == 0:\n                # Clean close - no data was read\n                raise\n            raise DirtyProtocolError(\n                f\"Incomplete header: got {len(e.partial)} bytes, \"\n                f\"expected {HEADER_SIZE}\",\n                raw_data=e.partial\n            )\n\n        msg_type, request_id, length = BinaryProtocol.decode_header(header)\n\n        # Read payload\n        if length > 0:\n            try:\n                payload_data = await reader.readexactly(length)\n            except asyncio.IncompleteReadError as e:\n                raise DirtyProtocolError(\n                    f\"Incomplete payload: got {len(e.partial)} bytes, \"\n                    f\"expected {length}\",\n                    raw_data=e.partial\n                )\n\n            try:\n                payload_dict = TLVEncoder.decode_full(payload_data)\n            except DirtyProtocolError:\n                raise\n            except Exception as e:\n                raise DirtyProtocolError(\n                    f\"Failed to decode TLV payload: {e}\",\n                    raw_data=payload_data[:50]\n                )\n        else:\n            payload_dict = {}\n\n        # Build response dict\n        msg_type_str = MSG_TYPE_TO_STR[msg_type]\n        result = {\"type\": msg_type_str, \"id\": request_id}\n        result.update(payload_dict)\n\n        return result\n\n    @staticmethod\n    async def write_message_async(writer: asyncio.StreamWriter,\n                                  message: dict) -> None:\n        \"\"\"\n        Write a message to async stream.\n\n        Accepts dict format for backwards compatibility.\n\n        Args:\n            writer: asyncio StreamWriter\n            message: Message dict with 'type', 'id', and payload fields\n\n        Raises:\n            DirtyProtocolError: If encoding fails\n            ConnectionError: If write fails\n        \"\"\"\n        data = BinaryProtocol._encode_from_dict(message)\n        writer.write(data)\n        await writer.drain()\n\n    # -------------------------------------------------------------------------\n    # Sync API (for HTTP workers that may not be async)\n    # -------------------------------------------------------------------------\n\n    @staticmethod\n    def _recv_exactly(sock: socket.socket, n: int) -> bytes:\n        \"\"\"\n        Receive exactly n bytes from a socket.\n\n        Args:\n            sock: Socket to read from\n            n: Number of bytes to read\n\n        Returns:\n            bytes: Received data\n\n        Raises:\n            DirtyProtocolError: If read fails or connection closed\n        \"\"\"\n        data = b\"\"\n        while len(data) < n:\n            chunk = sock.recv(n - len(data))\n            if not chunk:\n                if len(data) == 0:\n                    raise DirtyProtocolError(\"Connection closed\")\n                raise DirtyProtocolError(\n                    f\"Connection closed after {len(data)} bytes, expected {n}\",\n                    raw_data=data\n                )\n            data += chunk\n        return data\n\n    @staticmethod\n    def read_message(sock: socket.socket) -> dict:\n        \"\"\"\n        Read a complete message from socket (sync).\n\n        Args:\n            sock: Socket to read from\n\n        Returns:\n            dict: Message dict with 'type', 'id', and payload fields\n\n        Raises:\n            DirtyProtocolError: If read fails or message is malformed\n        \"\"\"\n        # Read header\n        header = BinaryProtocol._recv_exactly(sock, HEADER_SIZE)\n        msg_type, request_id, length = BinaryProtocol.decode_header(header)\n\n        # Read payload\n        if length > 0:\n            payload_data = BinaryProtocol._recv_exactly(sock, length)\n            try:\n                payload_dict = TLVEncoder.decode_full(payload_data)\n            except DirtyProtocolError:\n                raise\n            except Exception as e:\n                raise DirtyProtocolError(\n                    f\"Failed to decode TLV payload: {e}\",\n                    raw_data=payload_data[:50]\n                )\n        else:\n            payload_dict = {}\n\n        # Build response dict\n        msg_type_str = MSG_TYPE_TO_STR[msg_type]\n        result = {\"type\": msg_type_str, \"id\": request_id}\n        result.update(payload_dict)\n\n        return result\n\n    @staticmethod\n    def write_message(sock: socket.socket, message: dict) -> None:\n        \"\"\"\n        Write a message to socket (sync).\n\n        Args:\n            sock: Socket to write to\n            message: Message dict with 'type', 'id', and payload fields\n\n        Raises:\n            DirtyProtocolError: If encoding fails\n            OSError: If write fails\n        \"\"\"\n        data = BinaryProtocol._encode_from_dict(message)\n        sock.sendall(data)\n\n    @staticmethod\n    def _encode_from_dict(message: dict) -> bytes:  # pylint: disable=too-many-return-statements\n        \"\"\"\n        Encode a message dict to binary format.\n\n        Supports the old dict-based API for backwards compatibility.\n\n        Args:\n            message: Message dict with 'type', 'id', and payload fields\n\n        Returns:\n            bytes: Complete encoded message\n        \"\"\"\n        msg_type_str = message.get(\"type\")\n        request_id = message.get(\"id\", 0)\n\n        # Handle string or int request IDs\n        if isinstance(request_id, str):\n            # For backwards compat with UUID strings, hash to int\n            request_id = hash(request_id) & 0xFFFFFFFFFFFFFFFF\n\n        msg_type = MSG_TYPE_FROM_STR.get(msg_type_str)\n        if msg_type is None:\n            raise DirtyProtocolError(f\"Unknown message type: {msg_type_str}\")\n\n        if msg_type == MSG_TYPE_REQUEST:\n            return BinaryProtocol.encode_request(\n                request_id,\n                message.get(\"app_path\", \"\"),\n                message.get(\"action\", \"\"),\n                message.get(\"args\"),\n                message.get(\"kwargs\")\n            )\n        elif msg_type == MSG_TYPE_RESPONSE:\n            return BinaryProtocol.encode_response(\n                request_id,\n                message.get(\"result\")\n            )\n        elif msg_type == MSG_TYPE_ERROR:\n            return BinaryProtocol.encode_error(\n                request_id,\n                message.get(\"error\", {})\n            )\n        elif msg_type == MSG_TYPE_CHUNK:\n            return BinaryProtocol.encode_chunk(\n                request_id,\n                message.get(\"data\")\n            )\n        elif msg_type == MSG_TYPE_END:\n            return BinaryProtocol.encode_end(request_id)\n        elif msg_type == MSG_TYPE_STASH:\n            return BinaryProtocol.encode_stash(\n                request_id,\n                message.get(\"op\"),\n                message.get(\"table\", \"\"),\n                message.get(\"key\"),\n                message.get(\"value\"),\n                message.get(\"pattern\")\n            )\n        elif msg_type == MSG_TYPE_STATUS:\n            return BinaryProtocol.encode_status(request_id)\n        elif msg_type == MSG_TYPE_MANAGE:\n            return BinaryProtocol.encode_manage(\n                request_id,\n                message.get(\"op\"),\n                message.get(\"count\", 1)\n            )\n        else:\n            raise DirtyProtocolError(f\"Unhandled message type: {msg_type}\")\n\n\n# =============================================================================\n# Backwards Compatibility Aliases\n# =============================================================================\n\n# Alias BinaryProtocol as DirtyProtocol for drop-in replacement\nDirtyProtocol = BinaryProtocol\n\n\n# Message builder helpers (backwards compatible with old API)\ndef make_request(request_id, app_path: str, action: str,\n                 args: tuple = None, kwargs: dict = None) -> dict:\n    \"\"\"\n    Build a request message dict.\n\n    Args:\n        request_id: Unique request identifier (int or str)\n        app_path: Import path of the dirty app (e.g., 'myapp.ml:MLApp')\n        action: Action to call on the app\n        args: Positional arguments\n        kwargs: Keyword arguments\n\n    Returns:\n        dict: Request message dict\n    \"\"\"\n    return {\n        \"type\": DirtyProtocol.MSG_TYPE_REQUEST,\n        \"id\": request_id,\n        \"app_path\": app_path,\n        \"action\": action,\n        \"args\": list(args) if args else [],\n        \"kwargs\": kwargs or {},\n    }\n\n\ndef make_response(request_id, result) -> dict:\n    \"\"\"\n    Build a success response message dict.\n\n    Args:\n        request_id: Request identifier this responds to\n        result: Result value\n\n    Returns:\n        dict: Response message dict\n    \"\"\"\n    return {\n        \"type\": DirtyProtocol.MSG_TYPE_RESPONSE,\n        \"id\": request_id,\n        \"result\": result,\n    }\n\n\ndef make_error_response(request_id, error) -> dict:\n    \"\"\"\n    Build an error response message dict.\n\n    Args:\n        request_id: Request identifier this responds to\n        error: DirtyError instance or dict with error info\n\n    Returns:\n        dict: Error response message dict\n    \"\"\"\n    from .errors import DirtyError\n    if isinstance(error, DirtyError):\n        error_dict = error.to_dict()\n    elif isinstance(error, dict):\n        error_dict = error\n    else:\n        error_dict = {\n            \"error_type\": type(error).__name__,\n            \"message\": str(error),\n            \"details\": {},\n        }\n\n    return {\n        \"type\": DirtyProtocol.MSG_TYPE_ERROR,\n        \"id\": request_id,\n        \"error\": error_dict,\n    }\n\n\ndef make_chunk_message(request_id, data) -> dict:\n    \"\"\"\n    Build a chunk message dict for streaming responses.\n\n    Args:\n        request_id: Request identifier this chunk belongs to\n        data: Chunk data\n\n    Returns:\n        dict: Chunk message dict\n    \"\"\"\n    return {\n        \"type\": DirtyProtocol.MSG_TYPE_CHUNK,\n        \"id\": request_id,\n        \"data\": data,\n    }\n\n\ndef make_end_message(request_id) -> dict:\n    \"\"\"\n    Build an end-of-stream message dict.\n\n    Args:\n        request_id: Request identifier this ends\n\n    Returns:\n        dict: End message dict\n    \"\"\"\n    return {\n        \"type\": DirtyProtocol.MSG_TYPE_END,\n        \"id\": request_id,\n    }\n\n\ndef make_stash_message(request_id, op: int, table: str,\n                       key=None, value=None, pattern=None) -> dict:\n    \"\"\"\n    Build a stash operation message dict.\n\n    Args:\n        request_id: Unique request identifier (int or str)\n        op: Stash operation code (STASH_OP_*)\n        table: Table name\n        key: Optional key for put/get/delete operations\n        value: Optional value for put operation\n        pattern: Optional pattern for keys operation\n\n    Returns:\n        dict: Stash message dict\n    \"\"\"\n    msg = {\n        \"type\": DirtyProtocol.MSG_TYPE_STASH,\n        \"id\": request_id,\n        \"op\": op,\n        \"table\": table,\n    }\n    if key is not None:\n        msg[\"key\"] = key\n    if value is not None:\n        msg[\"value\"] = value\n    if pattern is not None:\n        msg[\"pattern\"] = pattern\n    return msg\n\n\ndef make_manage_message(request_id, op: int, count: int = 1) -> dict:\n    \"\"\"\n    Build a worker management message dict.\n\n    Args:\n        request_id: Unique request identifier (int or str)\n        op: Management operation (MANAGE_OP_ADD or MANAGE_OP_REMOVE)\n        count: Number of workers to add/remove\n\n    Returns:\n        dict: Manage message dict\n    \"\"\"\n    return {\n        \"type\": DirtyProtocol.MSG_TYPE_MANAGE,\n        \"id\": request_id,\n        \"op\": op,\n        \"count\": count,\n    }\n"
  },
  {
    "path": "gunicorn/dirty/stash.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nStash - Global Shared State for Dirty Workers\n\nProvides simple key-value tables stored in the arbiter process.\nAll workers can read and write to the same tables.\n\nUsage::\n\n    from gunicorn.dirty import stash\n\n    # Basic operations - table is auto-created on first access\n    stash.put(\"sessions\", \"user:1\", {\"name\": \"Alice\", \"role\": \"admin\"})\n    user = stash.get(\"sessions\", \"user:1\")\n    stash.delete(\"sessions\", \"user:1\")\n\n    # Dict-like interface\n    sessions = stash.table(\"sessions\")\n    sessions[\"user:1\"] = {\"name\": \"Alice\"}\n    user = sessions[\"user:1\"]\n    del sessions[\"user:1\"]\n\n    # Query operations\n    keys = stash.keys(\"sessions\")\n    keys = stash.keys(\"sessions\", pattern=\"user:*\")\n\n    # Table management\n    stash.ensure(\"cache\")           # Explicit creation (idempotent)\n    stash.clear(\"sessions\")         # Delete all entries\n    stash.delete_table(\"sessions\")  # Delete the table itself\n    tables = stash.tables()         # List all tables\n\nDeclarative usage in DirtyApp::\n\n    class MyApp(DirtyApp):\n        stashes = [\"sessions\", \"cache\"]  # Auto-created on arbiter start\n\n        def __call__(self, action, *args, **kwargs):\n            # Tables are ready to use\n            stash.put(\"sessions\", \"key\", \"value\")\n\nNote: Tables are stored in the arbiter process and are ephemeral.\nIf the arbiter restarts, all data is lost.\n\"\"\"\n\nimport threading\nimport uuid\n\nfrom .errors import DirtyError\nfrom .protocol import (\n    DirtyProtocol,\n    STASH_OP_PUT,\n    STASH_OP_GET,\n    STASH_OP_DELETE,\n    STASH_OP_KEYS,\n    STASH_OP_CLEAR,\n    STASH_OP_INFO,\n    STASH_OP_ENSURE,\n    STASH_OP_DELETE_TABLE,\n    STASH_OP_TABLES,\n    STASH_OP_EXISTS,\n    make_stash_message,\n)\n\n\nclass StashError(DirtyError):\n    \"\"\"Base exception for stash operations.\"\"\"\n\n\nclass StashTableNotFoundError(StashError):\n    \"\"\"Raised when a table does not exist.\"\"\"\n\n    def __init__(self, table_name):\n        self.table_name = table_name\n        super().__init__(f\"Stash table not found: {table_name}\")\n\n\nclass StashKeyNotFoundError(StashError):\n    \"\"\"Raised when a key does not exist in a table.\"\"\"\n\n    def __init__(self, table_name, key):\n        self.table_name = table_name\n        self.key = key\n        super().__init__(f\"Key not found in {table_name}: {key}\")\n\n\nclass StashClient:\n    \"\"\"\n    Client for stash operations.\n\n    Communicates with the arbiter which stores all tables in memory.\n    \"\"\"\n\n    def __init__(self, socket_path, timeout=30.0):\n        \"\"\"\n        Initialize the stash client.\n\n        Args:\n            socket_path: Path to the dirty arbiter's Unix socket\n            timeout: Default timeout for operations in seconds\n        \"\"\"\n        self.socket_path = socket_path\n        self.timeout = timeout\n        self._sock = None\n        self._lock = threading.Lock()\n\n    def _get_request_id(self):\n        \"\"\"Generate a unique request ID.\"\"\"\n        return str(uuid.uuid4())\n\n    def _connect(self):\n        \"\"\"Establish connection to arbiter.\"\"\"\n        import socket\n        if self._sock is not None:\n            return\n\n        try:\n            self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)\n            self._sock.settimeout(self.timeout)\n            self._sock.connect(self.socket_path)\n        except (socket.error, OSError) as e:\n            self._sock = None\n            raise StashError(f\"Failed to connect to arbiter: {e}\") from e\n\n    def _close(self):\n        \"\"\"Close the connection.\"\"\"\n        if self._sock is not None:\n            try:\n                self._sock.close()\n            except Exception:\n                pass\n            self._sock = None\n\n    def _execute(self, op, table, key=None, value=None, pattern=None):\n        \"\"\"\n        Execute a stash operation.\n\n        Args:\n            op: Operation code (STASH_OP_*)\n            table: Table name\n            key: Optional key\n            value: Optional value\n            pattern: Optional pattern for keys operation\n\n        Returns:\n            Result from the operation\n        \"\"\"\n        with self._lock:\n            if self._sock is None:\n                self._connect()\n\n            request_id = self._get_request_id()\n            message = make_stash_message(\n                request_id, op, table,\n                key=key, value=value, pattern=pattern\n            )\n\n            try:\n                DirtyProtocol.write_message(self._sock, message)\n                response = DirtyProtocol.read_message(self._sock)\n\n                msg_type = response.get(\"type\")\n                if msg_type == DirtyProtocol.MSG_TYPE_RESPONSE:\n                    return response.get(\"result\")\n                elif msg_type == DirtyProtocol.MSG_TYPE_ERROR:\n                    error_info = response.get(\"error\", {})\n                    error_type = error_info.get(\"error_type\", \"StashError\")\n                    error_msg = error_info.get(\"message\", \"Unknown error\")\n\n                    if error_type == \"StashTableNotFoundError\":\n                        raise StashTableNotFoundError(table)\n                    if error_type == \"StashKeyNotFoundError\":\n                        raise StashKeyNotFoundError(table, key)\n                    raise StashError(error_msg)\n                else:\n                    raise StashError(f\"Unexpected response type: {msg_type}\")\n\n            except Exception as e:\n                self._close()\n                if isinstance(e, StashError):\n                    raise\n                raise StashError(f\"Stash operation failed: {e}\") from e\n\n    # -------------------------------------------------------------------------\n    # Public API\n    # -------------------------------------------------------------------------\n\n    def put(self, table, key, value):\n        \"\"\"\n        Store a value in a table.\n\n        The table is automatically created if it doesn't exist.\n\n        Args:\n            table: Table name\n            key: Key to store under\n            value: Value to store (must be serializable)\n        \"\"\"\n        self._execute(STASH_OP_PUT, table, key=key, value=value)\n\n    def get(self, table, key, default=None):\n        \"\"\"\n        Retrieve a value from a table.\n\n        Args:\n            table: Table name\n            key: Key to retrieve\n            default: Default value if key not found\n\n        Returns:\n            The stored value, or default if not found\n        \"\"\"\n        try:\n            return self._execute(STASH_OP_GET, table, key=key)\n        except StashKeyNotFoundError:\n            return default\n\n    def delete(self, table, key):\n        \"\"\"\n        Delete a key from a table.\n\n        Args:\n            table: Table name\n            key: Key to delete\n\n        Returns:\n            True if key was deleted, False if it didn't exist\n        \"\"\"\n        return self._execute(STASH_OP_DELETE, table, key=key)\n\n    def keys(self, table, pattern=None):\n        \"\"\"\n        Get all keys in a table, optionally filtered by pattern.\n\n        Args:\n            table: Table name\n            pattern: Optional glob pattern (e.g., \"user:*\")\n\n        Returns:\n            List of keys\n        \"\"\"\n        return self._execute(STASH_OP_KEYS, table, pattern=pattern)\n\n    def clear(self, table):\n        \"\"\"\n        Delete all entries in a table.\n\n        Args:\n            table: Table name\n        \"\"\"\n        self._execute(STASH_OP_CLEAR, table)\n\n    def info(self, table):\n        \"\"\"\n        Get information about a table.\n\n        Args:\n            table: Table name\n\n        Returns:\n            Dict with table info (size, etc.)\n        \"\"\"\n        return self._execute(STASH_OP_INFO, table)\n\n    def ensure(self, table):\n        \"\"\"\n        Ensure a table exists (create if not exists).\n\n        This is idempotent - calling it multiple times is safe.\n\n        Args:\n            table: Table name\n        \"\"\"\n        self._execute(STASH_OP_ENSURE, table)\n\n    def exists(self, table, key=None):\n        \"\"\"\n        Check if a table or key exists.\n\n        Args:\n            table: Table name\n            key: Optional key to check within the table\n\n        Returns:\n            True if exists, False otherwise\n        \"\"\"\n        return self._execute(STASH_OP_EXISTS, table, key=key)\n\n    def delete_table(self, table):\n        \"\"\"\n        Delete an entire table.\n\n        Args:\n            table: Table name\n        \"\"\"\n        self._execute(STASH_OP_DELETE_TABLE, table)\n\n    def tables(self):\n        \"\"\"\n        List all tables.\n\n        Returns:\n            List of table names\n        \"\"\"\n        return self._execute(STASH_OP_TABLES, \"\")\n\n    def table(self, name):\n        \"\"\"\n        Get a dict-like interface to a table.\n\n        Args:\n            name: Table name\n\n        Returns:\n            StashTable instance\n        \"\"\"\n        return StashTable(self, name)\n\n    def close(self):\n        \"\"\"Close the client connection.\"\"\"\n        with self._lock:\n            self._close()\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        self.close()\n\n\nclass StashTable:\n    \"\"\"\n    Dict-like interface to a stash table.\n\n    Example::\n\n        sessions = stash.table(\"sessions\")\n        sessions[\"user:1\"] = {\"name\": \"Alice\"}\n        user = sessions[\"user:1\"]\n        del sessions[\"user:1\"]\n\n        # Iteration\n        for key in sessions:\n            print(key, sessions[key])\n    \"\"\"\n\n    def __init__(self, client, name):\n        self._client = client\n        self._name = name\n\n    @property\n    def name(self):\n        \"\"\"Table name.\"\"\"\n        return self._name\n\n    def __getitem__(self, key):\n        result = self._client.get(self._name, key)\n        if result is None:\n            # Check if key actually exists with None value\n            if not self._client.exists(self._name, key):\n                raise KeyError(key)\n        return result\n\n    def __setitem__(self, key, value):\n        self._client.put(self._name, key, value)\n\n    def __delitem__(self, key):\n        if not self._client.delete(self._name, key):\n            raise KeyError(key)\n\n    def __contains__(self, key):\n        return self._client.exists(self._name, key)\n\n    def __iter__(self):\n        return iter(self._client.keys(self._name))\n\n    def __len__(self):\n        info = self._client.info(self._name)\n        return info.get(\"size\", 0)\n\n    def get(self, key, default=None):\n        \"\"\"Get value with default.\"\"\"\n        return self._client.get(self._name, key, default)\n\n    def keys(self, pattern=None):\n        \"\"\"Get all keys, optionally filtered by pattern.\"\"\"\n        return self._client.keys(self._name, pattern=pattern)\n\n    def clear(self):\n        \"\"\"Delete all entries.\"\"\"\n        self._client.clear(self._name)\n\n    def items(self):\n        \"\"\"Iterate over (key, value) pairs.\"\"\"\n        for key in self._client.keys(self._name):\n            yield key, self._client.get(self._name, key)\n\n    def values(self):\n        \"\"\"Iterate over values.\"\"\"\n        for key in self._client.keys(self._name):\n            yield self._client.get(self._name, key)\n\n\n# =============================================================================\n# Global stash instance (module-level API)\n# =============================================================================\n\n# Thread-local storage for stash clients\n_thread_local = threading.local()\n\n# Global socket path\n_stash_socket_path = None\n\n\ndef set_stash_socket_path(path):\n    \"\"\"Set the global stash socket path (called during initialization).\"\"\"\n    global _stash_socket_path  # pylint: disable=global-statement\n    _stash_socket_path = path\n\n\ndef get_stash_socket_path():\n    \"\"\"Get the stash socket path.\"\"\"\n    import os\n    if _stash_socket_path is None:\n        # Check environment variable\n        path = os.environ.get('GUNICORN_DIRTY_SOCKET')\n        if path:\n            return path\n        raise StashError(\n            \"Stash socket path not configured. \"\n            \"Make sure dirty_workers > 0 and dirty_apps are configured.\"\n        )\n    return _stash_socket_path\n\n\ndef _get_client():\n    \"\"\"Get or create a thread-local stash client.\"\"\"\n    client = getattr(_thread_local, 'stash_client', None)\n    if client is None:\n        socket_path = get_stash_socket_path()\n        client = StashClient(socket_path)\n        _thread_local.stash_client = client\n    return client\n\n\n# Module-level functions that use the thread-local client\n\ndef put(table, key, value):\n    \"\"\"Store a value in a table.\"\"\"\n    _get_client().put(table, key, value)\n\n\ndef get(table, key, default=None):\n    \"\"\"Retrieve a value from a table.\"\"\"\n    return _get_client().get(table, key, default)\n\n\ndef delete(table, key):\n    \"\"\"Delete a key from a table.\"\"\"\n    return _get_client().delete(table, key)\n\n\ndef keys(table, pattern=None):\n    \"\"\"Get all keys in a table.\"\"\"\n    return _get_client().keys(table, pattern)\n\n\ndef clear(table):\n    \"\"\"Delete all entries in a table.\"\"\"\n    _get_client().clear(table)\n\n\ndef info(table):\n    \"\"\"Get information about a table.\"\"\"\n    return _get_client().info(table)\n\n\ndef ensure(table):\n    \"\"\"Ensure a table exists.\"\"\"\n    _get_client().ensure(table)\n\n\ndef exists(table, key=None):\n    \"\"\"Check if a table or key exists.\"\"\"\n    return _get_client().exists(table, key)\n\n\ndef delete_table(table):\n    \"\"\"Delete an entire table.\"\"\"\n    _get_client().delete_table(table)\n\n\ndef tables():\n    \"\"\"List all tables.\"\"\"\n    return _get_client().tables()\n\n\ndef table(name):\n    \"\"\"Get a dict-like interface to a table.\"\"\"\n    return _get_client().table(name)\n"
  },
  {
    "path": "gunicorn/dirty/tlv.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nTLV (Type-Length-Value) Binary Encoder/Decoder\n\nProvides efficient binary serialization for dirty worker protocol messages.\nInspired by OpenBSD msgctl/msgsnd message format.\n\nType Codes:\n    0x00: None (no value bytes)\n    0x01: bool (1 byte: 0x00 or 0x01)\n    0x05: int64 (8 bytes big-endian signed)\n    0x06: float64 (8 bytes IEEE 754)\n    0x10: bytes (4-byte length + raw bytes)\n    0x11: string (4-byte length + UTF-8 encoded)\n    0x20: list (4-byte count + encoded elements)\n    0x21: dict (4-byte count + encoded key-value pairs)\n\"\"\"\n\nimport struct\n\nfrom .errors import DirtyProtocolError\n\n\n# Type codes\nTYPE_NONE = 0x00\nTYPE_BOOL = 0x01\nTYPE_INT64 = 0x05\nTYPE_FLOAT64 = 0x06\nTYPE_BYTES = 0x10\nTYPE_STRING = 0x11\nTYPE_LIST = 0x20\nTYPE_DICT = 0x21\n\n# Maximum sizes for safety\nMAX_STRING_SIZE = 64 * 1024 * 1024  # 64 MB\nMAX_BYTES_SIZE = 64 * 1024 * 1024   # 64 MB\nMAX_LIST_SIZE = 1024 * 1024         # 1 million items\nMAX_DICT_SIZE = 1024 * 1024         # 1 million items\n\n\nclass TLVEncoder:\n    \"\"\"\n    TLV binary encoder/decoder.\n\n    Encodes Python values to binary TLV format and decodes back.\n    Supports: None, bool, int, float, bytes, str, list, dict.\n    \"\"\"\n\n    @staticmethod\n    def encode(value) -> bytes:  # pylint: disable=too-many-return-statements\n        \"\"\"\n        Encode a Python value to TLV binary format.\n\n        Args:\n            value: Python value to encode (None, bool, int, float,\n                   bytes, str, list, or dict)\n\n        Returns:\n            bytes: TLV-encoded binary data\n\n        Raises:\n            DirtyProtocolError: If value type is not supported\n        \"\"\"\n        if value is None:\n            return bytes([TYPE_NONE])\n\n        if isinstance(value, bool):\n            # bool must come before int since bool is a subclass of int\n            return bytes([TYPE_BOOL, 0x01 if value else 0x00])\n\n        if isinstance(value, int):\n            return bytes([TYPE_INT64]) + struct.pack(\">q\", value)\n\n        if isinstance(value, float):\n            return bytes([TYPE_FLOAT64]) + struct.pack(\">d\", value)\n\n        if isinstance(value, bytes):\n            if len(value) > MAX_BYTES_SIZE:\n                raise DirtyProtocolError(\n                    f\"Bytes too large: {len(value)} bytes \"\n                    f\"(max: {MAX_BYTES_SIZE})\"\n                )\n            return bytes([TYPE_BYTES]) + struct.pack(\">I\", len(value)) + value\n\n        if isinstance(value, str):\n            encoded = value.encode(\"utf-8\")\n            if len(encoded) > MAX_STRING_SIZE:\n                raise DirtyProtocolError(\n                    f\"String too large: {len(encoded)} bytes \"\n                    f\"(max: {MAX_STRING_SIZE})\"\n                )\n            return bytes([TYPE_STRING]) + struct.pack(\">I\", len(encoded)) + encoded\n\n        if isinstance(value, (list, tuple)):\n            if len(value) > MAX_LIST_SIZE:\n                raise DirtyProtocolError(\n                    f\"List too large: {len(value)} items \"\n                    f\"(max: {MAX_LIST_SIZE})\"\n                )\n            parts = [bytes([TYPE_LIST]), struct.pack(\">I\", len(value))]\n            for item in value:\n                parts.append(TLVEncoder.encode(item))\n            return b\"\".join(parts)\n\n        if isinstance(value, dict):\n            if len(value) > MAX_DICT_SIZE:\n                raise DirtyProtocolError(\n                    f\"Dict too large: {len(value)} items \"\n                    f\"(max: {MAX_DICT_SIZE})\"\n                )\n            parts = [bytes([TYPE_DICT]), struct.pack(\">I\", len(value))]\n            for k, v in value.items():\n                # Convert keys to strings (like JSON)\n                if not isinstance(k, str):\n                    k = str(k)\n                parts.append(TLVEncoder.encode(k))\n                parts.append(TLVEncoder.encode(v))\n            return b\"\".join(parts)\n\n        raise DirtyProtocolError(\n            f\"Unsupported type for TLV encoding: {type(value).__name__}\"\n        )\n\n    @staticmethod\n    def decode(data: bytes, offset: int = 0) -> tuple:  # pylint: disable=too-many-return-statements\n        \"\"\"\n        Decode a TLV-encoded value from binary data.\n\n        Args:\n            data: Binary data to decode\n            offset: Starting offset in the data\n\n        Returns:\n            tuple: (decoded_value, new_offset)\n\n        Raises:\n            DirtyProtocolError: If data is malformed or truncated\n        \"\"\"\n        if offset >= len(data):\n            raise DirtyProtocolError(\n                \"Truncated TLV data: no type byte\",\n                raw_data=data[offset:offset + 20]\n            )\n\n        type_code = data[offset]\n        offset += 1\n\n        if type_code == TYPE_NONE:\n            return None, offset\n\n        if type_code == TYPE_BOOL:\n            if offset >= len(data):\n                raise DirtyProtocolError(\n                    \"Truncated TLV data: missing bool value\",\n                    raw_data=data[offset - 1:offset + 20]\n                )\n            value = data[offset] != 0x00\n            return value, offset + 1\n\n        if type_code == TYPE_INT64:\n            if offset + 8 > len(data):\n                raise DirtyProtocolError(\n                    \"Truncated TLV data: incomplete int64\",\n                    raw_data=data[offset - 1:offset + 20]\n                )\n            value = struct.unpack(\">q\", data[offset:offset + 8])[0]\n            return value, offset + 8\n\n        if type_code == TYPE_FLOAT64:\n            if offset + 8 > len(data):\n                raise DirtyProtocolError(\n                    \"Truncated TLV data: incomplete float64\",\n                    raw_data=data[offset - 1:offset + 20]\n                )\n            value = struct.unpack(\">d\", data[offset:offset + 8])[0]\n            return value, offset + 8\n\n        if type_code == TYPE_BYTES:\n            if offset + 4 > len(data):\n                raise DirtyProtocolError(\n                    \"Truncated TLV data: incomplete bytes length\",\n                    raw_data=data[offset - 1:offset + 20]\n                )\n            length = struct.unpack(\">I\", data[offset:offset + 4])[0]\n            offset += 4\n\n            if length > MAX_BYTES_SIZE:\n                raise DirtyProtocolError(\n                    f\"Bytes too large: {length} bytes (max: {MAX_BYTES_SIZE})\"\n                )\n\n            if offset + length > len(data):\n                raise DirtyProtocolError(\n                    f\"Truncated TLV data: expected {length} bytes, \"\n                    f\"got {len(data) - offset}\",\n                    raw_data=data[offset - 5:offset + 20]\n                )\n            value = data[offset:offset + length]\n            return value, offset + length\n\n        if type_code == TYPE_STRING:\n            if offset + 4 > len(data):\n                raise DirtyProtocolError(\n                    \"Truncated TLV data: incomplete string length\",\n                    raw_data=data[offset - 1:offset + 20]\n                )\n            length = struct.unpack(\">I\", data[offset:offset + 4])[0]\n            offset += 4\n\n            if length > MAX_STRING_SIZE:\n                raise DirtyProtocolError(\n                    f\"String too large: {length} bytes (max: {MAX_STRING_SIZE})\"\n                )\n\n            if offset + length > len(data):\n                raise DirtyProtocolError(\n                    f\"Truncated TLV data: expected {length} bytes for string, \"\n                    f\"got {len(data) - offset}\",\n                    raw_data=data[offset - 5:offset + 20]\n                )\n            try:\n                value = data[offset:offset + length].decode(\"utf-8\")\n            except UnicodeDecodeError as e:\n                raise DirtyProtocolError(\n                    f\"Invalid UTF-8 in string: {e}\",\n                    raw_data=data[offset:offset + min(length, 20)]\n                )\n            return value, offset + length\n\n        if type_code == TYPE_LIST:\n            if offset + 4 > len(data):\n                raise DirtyProtocolError(\n                    \"Truncated TLV data: incomplete list count\",\n                    raw_data=data[offset - 1:offset + 20]\n                )\n            count = struct.unpack(\">I\", data[offset:offset + 4])[0]\n            offset += 4\n\n            if count > MAX_LIST_SIZE:\n                raise DirtyProtocolError(\n                    f\"List too large: {count} items (max: {MAX_LIST_SIZE})\"\n                )\n\n            items = []\n            for _ in range(count):\n                item, offset = TLVEncoder.decode(data, offset)\n                items.append(item)\n            return items, offset\n\n        if type_code == TYPE_DICT:\n            if offset + 4 > len(data):\n                raise DirtyProtocolError(\n                    \"Truncated TLV data: incomplete dict count\",\n                    raw_data=data[offset - 1:offset + 20]\n                )\n            count = struct.unpack(\">I\", data[offset:offset + 4])[0]\n            offset += 4\n\n            if count > MAX_DICT_SIZE:\n                raise DirtyProtocolError(\n                    f\"Dict too large: {count} items (max: {MAX_DICT_SIZE})\"\n                )\n\n            result = {}\n            for _ in range(count):\n                key, offset = TLVEncoder.decode(data, offset)\n                if not isinstance(key, str):\n                    raise DirtyProtocolError(\n                        f\"Dict key must be string, got {type(key).__name__}\"\n                    )\n                value, offset = TLVEncoder.decode(data, offset)\n                result[key] = value\n            return result, offset\n\n        raise DirtyProtocolError(\n            f\"Unknown TLV type code: 0x{type_code:02x}\",\n            raw_data=data[offset - 1:offset + 20]\n        )\n\n    @staticmethod\n    def decode_full(data: bytes):\n        \"\"\"\n        Decode a complete TLV-encoded value, ensuring all data is consumed.\n\n        Args:\n            data: Binary data to decode\n\n        Returns:\n            Decoded Python value\n\n        Raises:\n            DirtyProtocolError: If data is malformed or has trailing bytes\n        \"\"\"\n        value, offset = TLVEncoder.decode(data, 0)\n        if offset != len(data):\n            raise DirtyProtocolError(\n                f\"Trailing data after TLV: {len(data) - offset} bytes\",\n                raw_data=data[offset:offset + 20]\n            )\n        return value\n"
  },
  {
    "path": "gunicorn/dirty/worker.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nDirty Worker Process\n\nAsyncio-based worker that loads dirty apps and handles requests\nfrom the DirtyArbiter.\n\nThreading Model\n---------------\nEach dirty worker runs an asyncio event loop in the main thread for:\n- Handling connections from the arbiter\n- Managing heartbeat updates\n- Coordinating task execution\n\nActual app execution runs in a ThreadPoolExecutor (separate threads):\n- The number of threads is controlled by ``dirty_threads`` config (default: 1)\n- Each thread can execute one app action at a time\n- The asyncio event loop is NOT blocked by task execution\n\nState and Global Objects\n------------------------\nApps can maintain persistent state because:\n\n1. Apps are loaded ONCE when the worker starts (in ``load_apps()``)\n2. The same app instances are reused for ALL requests\n3. App state (instance variables, loaded models, etc.) persists\n\nExample::\n\n    class MLApp(DirtyApp):\n        def init(self):\n            self.model = load_heavy_model()  # Loaded once, reused\n            self.cache = {}                   # Persistent cache\n\n        def predict(self, data):\n            return self.model.predict(data)  # Uses loaded model\n\nThread Safety:\n- With ``dirty_threads=1`` (default): No concurrent access, thread-safe by design\n- With ``dirty_threads > 1``: Multiple threads share the same app instances,\n  apps MUST be thread-safe (use locks, thread-local storage, etc.)\n\nHeartbeat and Liveness\n----------------------\nThe worker sends heartbeat updates to prove it's alive:\n\n1. A dedicated asyncio task (``_heartbeat_loop``) runs independently\n2. It updates the heartbeat file every ``dirty_timeout / 2`` seconds\n3. Since tasks run in executor threads, they do NOT block heartbeats\n4. The arbiter kills workers that miss heartbeat updates\n\nTimeout Control\n---------------\nExecution timeout is enforced at two levels:\n\n1. **Worker level**: Each task execution has a timeout (``dirty_timeout``).\n   If exceeded, the worker returns a timeout error but the thread may\n   continue running (Python threads cannot be cancelled).\n\n2. **Arbiter level**: The arbiter also enforces timeout when waiting\n   for worker response. Workers that don't respond are killed via SIGABRT.\n\nNote: Since Python threads cannot be forcibly cancelled, a truly stuck\noperation will continue until the worker is killed by the arbiter.\n\"\"\"\n\nimport asyncio\nimport inspect\nimport os\nimport signal\nimport traceback\nimport uuid\n\nfrom gunicorn import util\nfrom gunicorn.workers.workertmp import WorkerTmp\n\nfrom .app import load_dirty_apps\nfrom .errors import (\n    DirtyAppError,\n    DirtyAppNotFoundError,\n    DirtyTimeoutError,\n    DirtyWorkerError,\n)\nfrom .protocol import (\n    DirtyProtocol,\n    make_response,\n    make_error_response,\n    make_chunk_message,\n    make_end_message,\n)\n\n\nclass DirtyWorker:\n    \"\"\"\n    Dirty worker process that loads dirty apps and handles requests.\n\n    Each worker runs its own asyncio event loop and listens on a\n    worker-specific Unix socket for requests from the DirtyArbiter.\n    \"\"\"\n\n    SIGNALS = [getattr(signal, \"SIG%s\" % x) for x in\n               \"ABRT HUP QUIT INT TERM USR1\".split()]\n\n    def __init__(self, age, ppid, app_paths, cfg, log, socket_path):\n        \"\"\"\n        Initialize a dirty worker.\n\n        Args:\n            age: Worker age (for identifying workers)\n            ppid: Parent process ID\n            app_paths: List of dirty app import paths\n            cfg: Gunicorn config\n            log: Logger\n            socket_path: Path to this worker's Unix socket\n        \"\"\"\n        self.age = age\n        self.pid = \"[booting]\"\n        self.ppid = ppid\n        self.app_paths = app_paths\n        self.cfg = cfg\n        self.log = log\n        self.socket_path = socket_path\n        self.booted = False\n        self.aborted = False\n        self.alive = True\n        self.tmp = WorkerTmp(cfg)\n        self.apps = {}\n        self._server = None\n        self._loop = None\n        self._executor = None\n\n    def __str__(self):\n        return f\"<DirtyWorker {self.pid}>\"\n\n    def notify(self):\n        \"\"\"Update heartbeat timestamp.\"\"\"\n        self.tmp.notify()\n\n    def init_process(self):\n        \"\"\"\n        Initialize the worker process after fork.\n\n        This is called in the child process after fork. It sets up\n        the environment, loads apps, and starts the main run loop.\n        \"\"\"\n        # Set environment variables\n        if self.cfg.env:\n            for k, v in self.cfg.env.items():\n                os.environ[k] = v\n\n        util.set_owner_process(self.cfg.uid, self.cfg.gid,\n                               initgroups=self.cfg.initgroups)\n\n        # Reseed random number generator\n        util.seed()\n\n        # Prevent fd inheritance\n        util.close_on_exec(self.tmp.fileno())\n        self.log.close_on_exec()\n\n        # Set up signals\n        self.init_signals()\n\n        # Load dirty apps\n        self.load_apps()\n\n        # Call hook\n        self.pid = os.getpid()\n        self.cfg.dirty_worker_init(self)\n\n        # Enter main run loop\n        self.booted = True\n        self.run()\n\n    def init_signals(self):\n        \"\"\"Set up signal handlers.\"\"\"\n        # Reset signal handlers from parent\n        for sig in self.SIGNALS:\n            signal.signal(sig, signal.SIG_DFL)\n\n        # Handle graceful shutdown\n        signal.signal(signal.SIGTERM, self._signal_handler)\n        signal.signal(signal.SIGQUIT, self._signal_handler)\n        signal.signal(signal.SIGINT, self._signal_handler)\n\n        # Handle abort (timeout)\n        signal.signal(signal.SIGABRT, self._signal_handler)\n\n        # Handle USR1 (reopen logs)\n        signal.signal(signal.SIGUSR1, self._signal_handler)\n\n    def _signal_handler(self, sig, frame):\n        \"\"\"Handle signals by setting alive = False.\"\"\"\n        if sig == signal.SIGUSR1:\n            self.log.reopen_files()\n            return\n\n        self.alive = False\n        if self._loop:\n            self._loop.call_soon_threadsafe(self._shutdown)\n\n    def _shutdown(self):\n        \"\"\"Initiate async shutdown.\"\"\"\n        if self._server:\n            self._server.close()\n\n    def load_apps(self):\n        \"\"\"Load all configured dirty apps.\"\"\"\n        try:\n            self.apps = load_dirty_apps(self.app_paths)\n            for path, app in self.apps.items():\n                self.log.debug(\"Loaded dirty app: %s\", path)\n                try:\n                    app.init()\n                    self.log.info(\"Initialized dirty app: %s\", path)\n                except Exception as e:\n                    self.log.error(\"Failed to initialize dirty app %s: %s\",\n                                   path, e)\n                    raise\n        except Exception as e:\n            self.log.error(\"Failed to load dirty apps: %s\", e)\n            raise\n\n    def run(self):\n        \"\"\"Run the main asyncio event loop.\"\"\"\n        # Lazy import for gevent compatibility (see #3482)\n        from concurrent.futures import ThreadPoolExecutor\n\n        # Create thread pool for executing app actions\n        num_threads = self.cfg.dirty_threads\n        self._executor = ThreadPoolExecutor(\n            max_workers=num_threads,\n            thread_name_prefix=f\"dirty-worker-{self.pid}-\"\n        )\n        self.log.debug(\"Created thread pool with %d threads\", num_threads)\n\n        try:\n            self._loop = asyncio.new_event_loop()\n            asyncio.set_event_loop(self._loop)\n            self._loop.run_until_complete(self._run_async())\n        except Exception as e:\n            self.log.error(\"Worker error: %s\", e)\n        finally:\n            self._cleanup()\n\n    async def _run_async(self):\n        \"\"\"Main async loop - start server and handle connections.\"\"\"\n        # Remove socket if it exists\n        if os.path.exists(self.socket_path):\n            os.unlink(self.socket_path)\n\n        # Start Unix socket server\n        self._server = await asyncio.start_unix_server(\n            self.handle_connection,\n            path=self.socket_path\n        )\n\n        # Make socket accessible\n        os.chmod(self.socket_path, 0o600)\n\n        self.log.info(\"Dirty worker %s listening on %s\",\n                      self.pid, self.socket_path)\n\n        # Start heartbeat task\n        heartbeat_task = asyncio.create_task(self._heartbeat_loop())\n\n        try:\n            async with self._server:\n                await self._server.serve_forever()\n        except asyncio.CancelledError:\n            pass\n        finally:\n            heartbeat_task.cancel()\n            try:\n                await heartbeat_task\n            except asyncio.CancelledError:\n                pass\n\n    async def _heartbeat_loop(self):\n        \"\"\"Periodically update heartbeat.\"\"\"\n        while self.alive:\n            self.notify()\n            await asyncio.sleep(self.cfg.dirty_timeout / 2.0)\n\n    async def handle_connection(self, reader, writer):\n        \"\"\"\n        Handle a connection from the arbiter.\n\n        Each connection can send multiple requests.\n        \"\"\"\n        self.log.debug(\"New connection from arbiter\")\n\n        try:\n            while self.alive:\n                try:\n                    message = await DirtyProtocol.read_message_async(reader)\n                except asyncio.IncompleteReadError:\n                    # Connection closed\n                    break\n\n                # Handle the request - pass writer for streaming support\n                await self.handle_request(message, writer)\n        except Exception as e:\n            self.log.error(\"Connection error: %s\", e)\n        finally:\n            writer.close()\n            try:\n                await writer.wait_closed()\n            except Exception:\n                pass\n\n    async def handle_request(self, message, writer):\n        \"\"\"\n        Handle a single request message.\n\n        Supports both regular (non-streaming) and streaming responses.\n        For streaming, detects if the result is a generator and sends\n        chunk messages followed by an end message.\n\n        Args:\n            message: Request dict from protocol\n            writer: StreamWriter for sending responses\n        \"\"\"\n        request_id = message.get(\"id\", str(uuid.uuid4()))\n        msg_type = message.get(\"type\")\n\n        if msg_type != DirtyProtocol.MSG_TYPE_REQUEST:\n            response = make_error_response(\n                request_id,\n                DirtyWorkerError(f\"Unknown message type: {msg_type}\")\n            )\n            await DirtyProtocol.write_message_async(writer, response)\n            return\n\n        app_path = message.get(\"app_path\")\n        action = message.get(\"action\")\n        args = message.get(\"args\", [])\n        kwargs = message.get(\"kwargs\", {})\n\n        # Update heartbeat before executing\n        self.notify()\n\n        try:\n            result = await self.execute(app_path, action, args, kwargs)\n\n            # Check if result is a generator (streaming)\n            if inspect.isgenerator(result):\n                await self._stream_sync_generator(request_id, result, writer)\n            elif inspect.isasyncgen(result):\n                await self._stream_async_generator(request_id, result, writer)\n            else:\n                # Regular non-streaming response\n                response = make_response(request_id, result)\n                await DirtyProtocol.write_message_async(writer, response)\n        except Exception as e:\n            tb = traceback.format_exc()\n            self.log.error(\"Error executing %s.%s: %s\\n%s\",\n                           app_path, action, e, tb)\n            response = make_error_response(\n                request_id,\n                DirtyAppError(str(e), app_path=app_path, action=action,\n                              traceback=tb)\n            )\n            await DirtyProtocol.write_message_async(writer, response)\n\n    async def _stream_sync_generator(self, request_id, gen, writer):\n        \"\"\"\n        Stream chunks from a synchronous generator.\n\n        Args:\n            request_id: Request ID for the messages\n            gen: Sync generator to iterate\n            writer: StreamWriter for sending messages\n        \"\"\"\n        # Sentinel value to detect end of generator\n        # (StopIteration cannot be raised into a Future in Python 3.7+)\n        _EXHAUSTED = object()\n\n        def _get_next():\n            try:\n                return next(gen)\n            except StopIteration:\n                return _EXHAUSTED\n\n        try:\n            loop = asyncio.get_running_loop()\n            while True:\n                # Run next() in executor to avoid blocking event loop\n                chunk = await loop.run_in_executor(self._executor, _get_next)\n                if chunk is _EXHAUSTED:\n                    break\n                # Send chunk message\n                await DirtyProtocol.write_message_async(\n                    writer, make_chunk_message(request_id, chunk)\n                )\n                # Update heartbeat during long streams\n                self.notify()\n            # Send end message\n            await DirtyProtocol.write_message_async(\n                writer, make_end_message(request_id)\n            )\n        except Exception as e:\n            # Error during streaming - send error message\n            tb = traceback.format_exc()\n            self.log.error(\"Error during streaming: %s\\n%s\", e, tb)\n            response = make_error_response(\n                request_id,\n                DirtyAppError(str(e), traceback=tb)\n            )\n            await DirtyProtocol.write_message_async(writer, response)\n        finally:\n            gen.close()\n\n    async def _stream_async_generator(self, request_id, gen, writer):\n        \"\"\"\n        Stream chunks from an asynchronous generator.\n\n        Args:\n            request_id: Request ID for the messages\n            gen: Async generator to iterate\n            writer: StreamWriter for sending messages\n        \"\"\"\n        try:\n            async for chunk in gen:\n                # Send chunk message\n                await DirtyProtocol.write_message_async(\n                    writer, make_chunk_message(request_id, chunk)\n                )\n                # Update heartbeat during long streams\n                self.notify()\n            # Send end message\n            await DirtyProtocol.write_message_async(\n                writer, make_end_message(request_id)\n            )\n        except Exception as e:\n            # Error during streaming - send error message\n            tb = traceback.format_exc()\n            self.log.error(\"Error during streaming: %s\\n%s\", e, tb)\n            response = make_error_response(\n                request_id,\n                DirtyAppError(str(e), traceback=tb)\n            )\n            await DirtyProtocol.write_message_async(writer, response)\n        finally:\n            await gen.aclose()\n\n    async def execute(self, app_path, action, args, kwargs):\n        \"\"\"\n        Execute an action on a dirty app.\n\n        The action runs in a thread pool executor to avoid blocking the\n        asyncio event loop. Execution timeout is enforced using\n        ``dirty_timeout`` config.\n\n        Args:\n            app_path: Import path of the dirty app\n            action: Action name to execute\n            args: Positional arguments\n            kwargs: Keyword arguments\n\n        Returns:\n            Result from the app action\n\n        Raises:\n            DirtyAppNotFoundError: If app is not loaded\n            DirtyTimeoutError: If execution exceeds timeout\n            DirtyAppError: If execution fails\n        \"\"\"\n        if app_path not in self.apps:\n            raise DirtyAppNotFoundError(app_path)\n\n        app = self.apps[app_path]\n        timeout = self.cfg.dirty_timeout if self.cfg.dirty_timeout > 0 else None\n\n        # Run the app call in the thread pool to avoid blocking\n        # the event loop for CPU-bound operations\n        loop = asyncio.get_running_loop()\n\n        try:\n            result = await asyncio.wait_for(\n                loop.run_in_executor(\n                    self._executor,\n                    lambda: app(action, *args, **kwargs)\n                ),\n                timeout=timeout\n            )\n            return result\n        except asyncio.TimeoutError:\n            # Note: The thread continues running - we just stop waiting\n            self.log.warning(\n                \"Execution timeout for %s.%s after %ds\",\n                app_path, action, timeout\n            )\n            raise DirtyTimeoutError(\n                f\"Execution of {app_path}.{action} timed out\",\n                timeout=timeout\n            )\n\n    def _cleanup(self):\n        \"\"\"Clean up resources on shutdown.\"\"\"\n        # Shutdown thread pool executor\n        if self._executor:\n            self._executor.shutdown(wait=False, cancel_futures=True)\n            self._executor = None\n\n        # Close all apps\n        for path, app in self.apps.items():\n            try:\n                app.close()\n                self.log.debug(\"Closed dirty app: %s\", path)\n            except Exception as e:\n                self.log.error(\"Error closing dirty app %s: %s\", path, e)\n\n        # Close temp file\n        try:\n            self.tmp.close()\n        except Exception:\n            pass\n\n        # Remove socket file\n        try:\n            if os.path.exists(self.socket_path):\n                os.unlink(self.socket_path)\n        except Exception:\n            pass\n\n        self.log.info(\"Dirty worker %s exiting\", self.pid)\n"
  },
  {
    "path": "gunicorn/errors.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n# We don't need to call super() in __init__ methods of our\n# BaseException and Exception classes because we also define\n# our own __str__ methods so there is no need to pass 'message'\n# to the base class to get a meaningful output from 'str(exc)'.\n# pylint: disable=super-init-not-called\n\n\n# we inherit from BaseException here to make sure to not be caught\n# at application level\nclass HaltServer(BaseException):\n    def __init__(self, reason, exit_status=1):\n        self.reason = reason\n        self.exit_status = exit_status\n\n    def __str__(self):\n        return \"<HaltServer %r %d>\" % (self.reason, self.exit_status)\n\n\nclass ConfigError(Exception):\n    \"\"\" Exception raised on config error \"\"\"\n\n\nclass AppImportError(Exception):\n    \"\"\" Exception raised when loading an application \"\"\"\n"
  },
  {
    "path": "gunicorn/glogging.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport base64\nimport binascii\nimport json\nimport time\nimport logging\nlogging.Logger.manager.emittedNoHandlerWarning = 1  # noqa\nfrom logging.config import dictConfig\nfrom logging.config import fileConfig\nimport os\nimport socket\nimport sys\nimport threading\nimport traceback\n\nfrom gunicorn import util\n\n\n# syslog facility codes\nSYSLOG_FACILITIES = {\n    \"auth\": 4,\n    \"authpriv\": 10,\n    \"cron\": 9,\n    \"daemon\": 3,\n    \"ftp\": 11,\n    \"kern\": 0,\n    \"lpr\": 6,\n    \"mail\": 2,\n    \"news\": 7,\n    \"security\": 4,  # DEPRECATED\n    \"syslog\": 5,\n    \"user\": 1,\n    \"uucp\": 8,\n    \"local0\": 16,\n    \"local1\": 17,\n    \"local2\": 18,\n    \"local3\": 19,\n    \"local4\": 20,\n    \"local5\": 21,\n    \"local6\": 22,\n    \"local7\": 23\n}\n\nCONFIG_DEFAULTS = {\n    \"version\": 1,\n    \"disable_existing_loggers\": False,\n    \"root\": {\"level\": \"INFO\", \"handlers\": [\"console\"]},\n    \"loggers\": {\n        \"gunicorn.error\": {\n            \"level\": \"INFO\",\n            \"handlers\": [\"error_console\"],\n            \"propagate\": True,\n            \"qualname\": \"gunicorn.error\"\n        },\n\n        \"gunicorn.access\": {\n            \"level\": \"INFO\",\n            \"handlers\": [\"console\"],\n            \"propagate\": True,\n            \"qualname\": \"gunicorn.access\"\n        }\n    },\n    \"handlers\": {\n        \"console\": {\n            \"class\": \"logging.StreamHandler\",\n            \"formatter\": \"generic\",\n            \"stream\": \"ext://sys.stdout\"\n        },\n        \"error_console\": {\n            \"class\": \"logging.StreamHandler\",\n            \"formatter\": \"generic\",\n            \"stream\": \"ext://sys.stderr\"\n        },\n    },\n    \"formatters\": {\n        \"generic\": {\n            \"format\": \"%(asctime)s [%(process)d] [%(levelname)s] %(message)s\",\n            \"datefmt\": \"[%Y-%m-%d %H:%M:%S %z]\",\n            \"class\": \"logging.Formatter\"\n        }\n    }\n}\n\n\ndef loggers():\n    \"\"\" get list of all loggers \"\"\"\n    root = logging.root\n    existing = list(root.manager.loggerDict.keys())\n    return [logging.getLogger(name) for name in existing]\n\n\nclass SafeAtoms(dict):\n\n    def __init__(self, atoms):\n        dict.__init__(self)\n        for key, value in atoms.items():\n            if isinstance(value, str):\n                self[key] = value.replace('\"', '\\\\\"')\n            else:\n                self[key] = value\n\n    def __getitem__(self, k):\n        if k.startswith(\"{\"):\n            kl = k.lower()\n            if kl in self:\n                return super().__getitem__(kl)\n            else:\n                return \"-\"\n        if k in self:\n            return super().__getitem__(k)\n        else:\n            return '-'\n\n\ndef parse_syslog_address(addr):\n\n    # unix domain socket type depends on backend\n    # SysLogHandler will try both when given None\n    if addr.startswith(\"unix://\"):\n        sock_type = None\n\n        # set socket type only if explicitly requested\n        parts = addr.split(\"#\", 1)\n        if len(parts) == 2:\n            addr = parts[0]\n            if parts[1] == \"dgram\":\n                sock_type = socket.SOCK_DGRAM\n\n        return (sock_type, addr.split(\"unix://\")[1])\n\n    if addr.startswith(\"udp://\"):\n        addr = addr.split(\"udp://\")[1]\n        socktype = socket.SOCK_DGRAM\n    elif addr.startswith(\"tcp://\"):\n        addr = addr.split(\"tcp://\")[1]\n        socktype = socket.SOCK_STREAM\n    else:\n        raise RuntimeError(\"invalid syslog address\")\n\n    if '[' in addr and ']' in addr:\n        host = addr.split(']')[0][1:].lower()\n    elif ':' in addr:\n        host = addr.split(':')[0].lower()\n    elif addr == \"\":\n        host = \"localhost\"\n    else:\n        host = addr.lower()\n\n    addr = addr.split(']')[-1]\n    if \":\" in addr:\n        port = addr.split(':', 1)[1]\n        if not port.isdigit():\n            raise RuntimeError(\"%r is not a valid port number.\" % port)\n        port = int(port)\n    else:\n        port = 514\n\n    return (socktype, (host, port))\n\n\nclass Logger:\n\n    LOG_LEVELS = {\n        \"critical\": logging.CRITICAL,\n        \"error\": logging.ERROR,\n        \"warning\": logging.WARNING,\n        \"info\": logging.INFO,\n        \"debug\": logging.DEBUG\n    }\n    loglevel = logging.INFO\n\n    error_fmt = r\"%(asctime)s [%(process)d] [%(levelname)s] %(message)s\"\n    datefmt = r\"[%Y-%m-%d %H:%M:%S %z]\"\n\n    access_fmt = \"%(message)s\"\n    syslog_fmt = \"[%(process)d] %(message)s\"\n\n    atoms_wrapper_class = SafeAtoms\n\n    def __init__(self, cfg):\n        self.error_log = logging.getLogger(\"gunicorn.error\")\n        self.error_log.propagate = False\n        self.access_log = logging.getLogger(\"gunicorn.access\")\n        self.access_log.propagate = False\n        self.error_handlers = []\n        self.access_handlers = []\n        self.logfile = None\n        self.lock = threading.Lock()\n        self.cfg = cfg\n        self.setup(cfg)\n\n    def setup(self, cfg):\n        self.loglevel = self.LOG_LEVELS.get(cfg.loglevel.lower(), logging.INFO)\n        self.error_log.setLevel(self.loglevel)\n        self.access_log.setLevel(logging.INFO)\n\n        # set gunicorn.error handler\n        if self.cfg.capture_output and cfg.errorlog != \"-\":\n            for stream in sys.stdout, sys.stderr:\n                stream.flush()\n\n            self.logfile = open(cfg.errorlog, 'a+')\n            os.dup2(self.logfile.fileno(), sys.stdout.fileno())\n            os.dup2(self.logfile.fileno(), sys.stderr.fileno())\n\n        self._set_handler(self.error_log, cfg.errorlog,\n                          logging.Formatter(self.error_fmt, self.datefmt))\n\n        # set gunicorn.access handler\n        if cfg.accesslog is not None:\n            self._set_handler(\n                self.access_log, cfg.accesslog,\n                fmt=logging.Formatter(self.access_fmt), stream=sys.stdout\n            )\n\n        # set syslog handler\n        if cfg.syslog:\n            self._set_syslog_handler(\n                self.error_log, cfg, self.syslog_fmt, \"error\"\n            )\n            if not cfg.disable_redirect_access_to_syslog:\n                self._set_syslog_handler(\n                    self.access_log, cfg, self.syslog_fmt, \"access\"\n                )\n\n        if cfg.logconfig_dict:\n            config = CONFIG_DEFAULTS.copy()\n            config.update(cfg.logconfig_dict)\n            try:\n                dictConfig(config)\n            except (\n                    AttributeError,\n                    ImportError,\n                    ValueError,\n                    TypeError\n            ) as exc:\n                raise RuntimeError(str(exc)) from exc\n        elif cfg.logconfig_json:\n            config = CONFIG_DEFAULTS.copy()\n            if os.path.exists(cfg.logconfig_json):\n                try:\n                    config_json = json.load(open(cfg.logconfig_json))\n                    config.update(config_json)\n                    dictConfig(config)\n                except (\n                    json.JSONDecodeError,\n                    AttributeError,\n                    ImportError,\n                    ValueError,\n                    TypeError\n                ) as exc:\n                    raise RuntimeError(str(exc)) from exc\n        elif cfg.logconfig:\n            if os.path.exists(cfg.logconfig):\n                defaults = CONFIG_DEFAULTS.copy()\n                defaults['__file__'] = cfg.logconfig\n                defaults['here'] = os.path.dirname(cfg.logconfig)\n                fileConfig(cfg.logconfig, defaults=defaults,\n                           disable_existing_loggers=False)\n            else:\n                msg = \"Error: log config '%s' not found\"\n                raise RuntimeError(msg % cfg.logconfig)\n\n    def critical(self, msg, *args, **kwargs):\n        self.error_log.critical(msg, *args, **kwargs)\n\n    def error(self, msg, *args, **kwargs):\n        self.error_log.error(msg, *args, **kwargs)\n\n    def warning(self, msg, *args, **kwargs):\n        self.error_log.warning(msg, *args, **kwargs)\n\n    def info(self, msg, *args, **kwargs):\n        self.error_log.info(msg, *args, **kwargs)\n\n    def debug(self, msg, *args, **kwargs):\n        self.error_log.debug(msg, *args, **kwargs)\n\n    def exception(self, msg, *args, **kwargs):\n        self.error_log.exception(msg, *args, **kwargs)\n\n    def log(self, lvl, msg, *args, **kwargs):\n        if isinstance(lvl, str):\n            lvl = self.LOG_LEVELS.get(lvl.lower(), logging.INFO)\n        self.error_log.log(lvl, msg, *args, **kwargs)\n\n    def atoms(self, resp, req, environ, request_time):\n        \"\"\" Gets atoms for log formatting.\n        \"\"\"\n        status = resp.status\n        if isinstance(status, str):\n            status = status.split(None, 1)[0]\n        atoms = {\n            'h': environ.get('REMOTE_ADDR', '-'),\n            'l': '-',\n            'u': self._get_user(environ) or '-',\n            't': self.now(),\n            'r': \"%s %s %s\" % (environ['REQUEST_METHOD'],\n                               environ['RAW_URI'],\n                               environ[\"SERVER_PROTOCOL\"]),\n            's': status,\n            'm': environ.get('REQUEST_METHOD'),\n            'U': environ.get('PATH_INFO'),\n            'q': environ.get('QUERY_STRING'),\n            'H': environ.get('SERVER_PROTOCOL'),\n            'b': getattr(resp, 'sent', None) is not None and str(resp.sent) or '-',\n            'B': getattr(resp, 'sent', None),\n            'f': environ.get('HTTP_REFERER', '-'),\n            'a': environ.get('HTTP_USER_AGENT', '-'),\n            'T': request_time.seconds,\n            'D': (request_time.seconds * 1000000) + request_time.microseconds,\n            'M': (request_time.seconds * 1000) + int(request_time.microseconds / 1000),\n            'L': \"%d.%06d\" % (request_time.seconds, request_time.microseconds),\n            'p': \"<%s>\" % os.getpid()\n        }\n\n        # add request headers\n        if hasattr(req, 'headers'):\n            req_headers = req.headers\n        else:\n            req_headers = req\n\n        if hasattr(req_headers, \"items\"):\n            req_headers = req_headers.items()\n\n        atoms.update({\"{%s}i\" % k.lower(): v for k, v in req_headers})\n\n        resp_headers = resp.headers\n        if hasattr(resp_headers, \"items\"):\n            resp_headers = resp_headers.items()\n\n        # add response headers\n        atoms.update({\"{%s}o\" % k.lower(): v for k, v in resp_headers})\n\n        # add environ variables\n        environ_variables = environ.items()\n        atoms.update({\"{%s}e\" % k.lower(): v for k, v in environ_variables})\n\n        return atoms\n\n    def access(self, resp, req, environ, request_time):\n        \"\"\" See http://httpd.apache.org/docs/2.0/logs.html#combined\n        for format details\n        \"\"\"\n\n        if not (self.cfg.accesslog or self.cfg.logconfig or\n           self.cfg.logconfig_dict or self.cfg.logconfig_json or\n           (self.cfg.syslog and not self.cfg.disable_redirect_access_to_syslog)):\n            return\n\n        # wrap atoms:\n        # - make sure atoms will be test case insensitively\n        # - if atom doesn't exist replace it by '-'\n        safe_atoms = self.atoms_wrapper_class(\n            self.atoms(resp, req, environ, request_time)\n        )\n\n        try:\n            self.access_log.info(self.cfg.access_log_format, safe_atoms)\n        except Exception:\n            self.error(traceback.format_exc())\n\n    def now(self):\n        \"\"\" return date in Apache Common Log Format \"\"\"\n        return time.strftime('[%d/%b/%Y:%H:%M:%S %z]')\n\n    def reopen_files(self):\n        if self.cfg.capture_output and self.cfg.errorlog != \"-\":\n            for stream in sys.stdout, sys.stderr:\n                stream.flush()\n\n            with self.lock:\n                if self.logfile is not None:\n                    self.logfile.close()\n                self.logfile = open(self.cfg.errorlog, 'a+')\n                os.dup2(self.logfile.fileno(), sys.stdout.fileno())\n                os.dup2(self.logfile.fileno(), sys.stderr.fileno())\n\n        for log in loggers():\n            for handler in log.handlers:\n                if isinstance(handler, logging.FileHandler):\n                    handler.acquire()\n                    try:\n                        if handler.stream:\n                            handler.close()\n                            handler.stream = handler._open()\n                    finally:\n                        handler.release()\n\n    def close_on_exec(self):\n        for log in loggers():\n            for handler in log.handlers:\n                if isinstance(handler, logging.FileHandler):\n                    handler.acquire()\n                    try:\n                        if handler.stream:\n                            util.close_on_exec(handler.stream.fileno())\n                    finally:\n                        handler.release()\n\n    def _get_gunicorn_handler(self, log):\n        for h in log.handlers:\n            if getattr(h, \"_gunicorn\", False):\n                return h\n\n    def _set_handler(self, log, output, fmt, stream=None):\n        # remove previous gunicorn log handler\n        h = self._get_gunicorn_handler(log)\n        if h:\n            log.handlers.remove(h)\n\n        if output is not None:\n            if output == \"-\":\n                h = logging.StreamHandler(stream)\n            else:\n                util.check_is_writable(output)\n                h = logging.FileHandler(output)\n                # make sure the user can reopen the file\n                try:\n                    os.chown(h.baseFilename, self.cfg.user, self.cfg.group)\n                except OSError:\n                    # it's probably OK there, we assume the user has given\n                    # /dev/null as a parameter.\n                    pass\n\n            h.setFormatter(fmt)\n            h._gunicorn = True\n            log.addHandler(h)\n\n    def _set_syslog_handler(self, log, cfg, fmt, name):\n        # setup format\n        prefix = cfg.syslog_prefix or cfg.proc_name.replace(\":\", \".\")\n\n        prefix = \"gunicorn.%s.%s\" % (prefix, name)\n\n        # set format\n        fmt = logging.Formatter(r\"%s: %s\" % (prefix, fmt))\n\n        # syslog facility\n        try:\n            facility = SYSLOG_FACILITIES[cfg.syslog_facility.lower()]\n        except KeyError as exc:\n            raise RuntimeError(\"unknown facility name\") from exc\n\n        # parse syslog address\n        socktype, addr = parse_syslog_address(cfg.syslog_addr)\n\n        # finally setup the syslog handler\n        h = logging.handlers.SysLogHandler(address=addr,\n                                           facility=facility, socktype=socktype)\n\n        h.setFormatter(fmt)\n        h._gunicorn = True\n        log.addHandler(h)\n\n    def _get_user(self, environ):\n        user = None\n        http_auth = environ.get(\"HTTP_AUTHORIZATION\")\n        if http_auth and http_auth.lower().startswith('basic'):\n            auth = http_auth.split(\" \", 1)\n            if len(auth) == 2:\n                try:\n                    # b64decode doesn't accept unicode in Python < 3.3\n                    # so we need to convert it to a byte string\n                    auth = base64.b64decode(auth[1].strip().encode('utf-8'))\n                    # b64decode returns a byte string\n                    user = auth.split(b\":\", 1)[0].decode(\"UTF-8\")\n                except (TypeError, binascii.Error, UnicodeDecodeError) as exc:\n                    self.debug(\"Couldn't get username: %s\", exc)\n        return user\n"
  },
  {
    "path": "gunicorn/http/__init__.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.message import Message, Request\nfrom gunicorn.http.parser import RequestParser\n\n\ndef get_parser(cfg, source, source_addr, http2_connection=False):\n    \"\"\"Get appropriate parser based on protocol config.\n\n    Args:\n        cfg: Gunicorn config object\n        source: Socket or iterable source\n        source_addr: Source address tuple or None\n        http2_connection: If True, create HTTP/2 connection handler\n\n    Returns:\n        Parser instance (RequestParser, UWSGIParser, or HTTP2ServerConnection)\n    \"\"\"\n    # HTTP/2 connection\n    if http2_connection:\n        from gunicorn.http2.connection import HTTP2ServerConnection\n        return HTTP2ServerConnection(cfg, source, source_addr)\n\n    # uWSGI protocol\n    protocol = getattr(cfg, 'protocol', 'http')\n    if protocol == 'uwsgi':\n        from gunicorn.uwsgi.parser import UWSGIParser\n        return UWSGIParser(cfg, source, source_addr)\n\n    # Default HTTP/1.x\n    return RequestParser(cfg, source, source_addr)\n\n\n__all__ = ['Message', 'Request', 'RequestParser', 'get_parser']\n"
  },
  {
    "path": "gunicorn/http/body.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport io\nimport sys\n\nfrom gunicorn.http.errors import (NoMoreData, ChunkMissingTerminator,\n                                  InvalidChunkSize)\n\n\nclass ChunkedReader:\n    def __init__(self, req, unreader):\n        self.req = req\n        self.parser = self.parse_chunked(unreader)\n        self.buf = io.BytesIO()\n\n    def read(self, size):\n        if not isinstance(size, int):\n            raise TypeError(\"size must be an integer type\")\n        if size < 0:\n            raise ValueError(\"Size must be positive.\")\n        if size == 0:\n            return b\"\"\n\n        if self.parser:\n            while self.buf.tell() < size:\n                try:\n                    self.buf.write(next(self.parser))\n                except StopIteration:\n                    self.parser = None\n                    break\n\n        data = self.buf.getvalue()\n        ret, rest = data[:size], data[size:]\n        self.buf = io.BytesIO()\n        self.buf.write(rest)\n        return ret\n\n    def parse_trailers(self, unreader, data):\n        buf = io.BytesIO()\n        buf.write(data)\n\n        idx = buf.getvalue().find(b\"\\r\\n\\r\\n\")\n        done = buf.getvalue()[:2] == b\"\\r\\n\"\n        while idx < 0 and not done:\n            self.get_data(unreader, buf)\n            idx = buf.getvalue().find(b\"\\r\\n\\r\\n\")\n            done = buf.getvalue()[:2] == b\"\\r\\n\"\n        if done:\n            unreader.unread(buf.getvalue()[2:])\n            return b\"\"\n        self.req.trailers = self.req.parse_headers(buf.getvalue()[:idx], from_trailer=True)\n        unreader.unread(buf.getvalue()[idx + 4:])\n\n    def parse_chunked(self, unreader):\n        (size, rest) = self.parse_chunk_size(unreader)\n        while size > 0:\n            while size > len(rest):\n                size -= len(rest)\n                yield rest\n                rest = unreader.read()\n                if not rest:\n                    raise NoMoreData()\n            yield rest[:size]\n            # Remove \\r\\n after chunk\n            rest = rest[size:]\n            while len(rest) < 2:\n                new_data = unreader.read()\n                if not new_data:\n                    break\n                rest += new_data\n            if rest[:2] != b'\\r\\n':\n                raise ChunkMissingTerminator(rest[:2])\n            (size, rest) = self.parse_chunk_size(unreader, data=rest[2:])\n\n    def parse_chunk_size(self, unreader, data=None):\n        buf = io.BytesIO()\n        if data is not None:\n            buf.write(data)\n\n        idx = buf.getvalue().find(b\"\\r\\n\")\n        while idx < 0:\n            self.get_data(unreader, buf)\n            idx = buf.getvalue().find(b\"\\r\\n\")\n\n        data = buf.getvalue()\n        line, rest_chunk = data[:idx], data[idx + 2:]\n\n        # RFC9112 7.1.1: BWS before chunk-ext - but ONLY then\n        chunk_size, *chunk_ext = line.split(b\";\", 1)\n        if chunk_ext:\n            chunk_size = chunk_size.rstrip(b\" \\t\")\n        if any(n not in b\"0123456789abcdefABCDEF\" for n in chunk_size):\n            raise InvalidChunkSize(chunk_size)\n        if len(chunk_size) == 0:\n            raise InvalidChunkSize(chunk_size)\n        chunk_size = int(chunk_size, 16)\n\n        if chunk_size == 0:\n            try:\n                self.parse_trailers(unreader, rest_chunk)\n            except NoMoreData:\n                pass\n            return (0, None)\n        return (chunk_size, rest_chunk)\n\n    def get_data(self, unreader, buf):\n        data = unreader.read()\n        if not data:\n            raise NoMoreData()\n        buf.write(data)\n\n\nclass LengthReader:\n    def __init__(self, unreader, length):\n        self.unreader = unreader\n        self.length = length\n\n    def read(self, size):\n        if not isinstance(size, int):\n            raise TypeError(\"size must be an integral type\")\n\n        size = min(self.length, size)\n        if size < 0:\n            raise ValueError(\"Size must be positive.\")\n        if size == 0:\n            return b\"\"\n\n        buf = io.BytesIO()\n        data = self.unreader.read()\n        while data:\n            buf.write(data)\n            if buf.tell() >= size:\n                break\n            data = self.unreader.read()\n\n        buf = buf.getvalue()\n        ret, rest = buf[:size], buf[size:]\n        self.unreader.unread(rest)\n        self.length -= size\n        return ret\n\n\nclass EOFReader:\n    def __init__(self, unreader):\n        self.unreader = unreader\n        self.buf = io.BytesIO()\n        self.finished = False\n\n    def read(self, size):\n        if not isinstance(size, int):\n            raise TypeError(\"size must be an integral type\")\n        if size < 0:\n            raise ValueError(\"Size must be positive.\")\n        if size == 0:\n            return b\"\"\n\n        if self.finished:\n            data = self.buf.getvalue()\n            ret, rest = data[:size], data[size:]\n            self.buf = io.BytesIO()\n            self.buf.write(rest)\n            return ret\n\n        data = self.unreader.read()\n        while data:\n            self.buf.write(data)\n            if self.buf.tell() > size:\n                break\n            data = self.unreader.read()\n\n        if not data:\n            self.finished = True\n\n        data = self.buf.getvalue()\n        ret, rest = data[:size], data[size:]\n        self.buf = io.BytesIO()\n        self.buf.write(rest)\n        return ret\n\n\nclass Body:\n    def __init__(self, reader):\n        self.reader = reader\n        self.buf = io.BytesIO()\n\n    def __iter__(self):\n        return self\n\n    def __next__(self):\n        ret = self.readline()\n        if not ret:\n            raise StopIteration()\n        return ret\n\n    next = __next__\n\n    def getsize(self, size):\n        if size is None:\n            return sys.maxsize\n        elif not isinstance(size, int):\n            raise TypeError(\"size must be an integral type\")\n        elif size < 0:\n            return sys.maxsize\n        return size\n\n    def read(self, size=None):\n        size = self.getsize(size)\n        if size == 0:\n            return b\"\"\n\n        if size < self.buf.tell():\n            data = self.buf.getvalue()\n            ret, rest = data[:size], data[size:]\n            self.buf = io.BytesIO()\n            self.buf.write(rest)\n            return ret\n\n        while size > self.buf.tell():\n            data = self.reader.read(1024)\n            if not data:\n                break\n            self.buf.write(data)\n\n        data = self.buf.getvalue()\n        ret, rest = data[:size], data[size:]\n        self.buf = io.BytesIO()\n        self.buf.write(rest)\n        return ret\n\n    def readline(self, size=None):\n        size = self.getsize(size)\n        if size == 0:\n            return b\"\"\n\n        data = self.buf.getvalue()\n        self.buf = io.BytesIO()\n\n        ret = []\n        while 1:\n            idx = data.find(b\"\\n\", 0, size)\n            idx = idx + 1 if idx >= 0 else size if len(data) >= size else 0\n            if idx:\n                ret.append(data[:idx])\n                self.buf.write(data[idx:])\n                break\n\n            ret.append(data)\n            size -= len(data)\n            data = self.reader.read(min(1024, size))\n            if not data:\n                break\n\n        return b\"\".join(ret)\n\n    def readlines(self, size=None):\n        ret = []\n        data = self.read()\n        while data:\n            pos = data.find(b\"\\n\")\n            if pos < 0:\n                ret.append(data)\n                data = b\"\"\n            else:\n                line, data = data[:pos + 1], data[pos + 1:]\n                ret.append(line)\n        return ret\n"
  },
  {
    "path": "gunicorn/http/errors.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n# We don't need to call super() in __init__ methods of our\n# BaseException and Exception classes because we also define\n# our own __str__ methods so there is no need to pass 'message'\n# to the base class to get a meaningful output from 'str(exc)'.\n# pylint: disable=super-init-not-called\n\n\nclass ParseException(Exception):\n    pass\n\n\nclass NoMoreData(IOError):\n    def __init__(self, buf=None):\n        self.buf = buf\n\n    def __str__(self):\n        return \"No more data after: %r\" % self.buf\n\n\nclass ConfigurationProblem(ParseException):\n    def __init__(self, info):\n        self.info = info\n        self.code = 500\n\n    def __str__(self):\n        return \"Configuration problem: %s\" % self.info\n\n\nclass InvalidRequestLine(ParseException):\n    def __init__(self, req):\n        self.req = req\n        self.code = 400\n\n    def __str__(self):\n        return \"Invalid HTTP request line: %r\" % self.req\n\n\nclass InvalidRequestMethod(ParseException):\n    def __init__(self, method):\n        self.method = method\n\n    def __str__(self):\n        return \"Invalid HTTP method: %r\" % self.method\n\n\nclass ExpectationFailed(ParseException):\n    def __init__(self, expect):\n        self.expect = expect\n\n    def __str__(self):\n        return \"Unable to comply with expectation: %r\" % (self.expect, )\n\n\nclass InvalidHTTPVersion(ParseException):\n    def __init__(self, version):\n        self.version = version\n\n    def __str__(self):\n        return \"Invalid HTTP Version: %r\" % (self.version,)\n\n\nclass InvalidHeader(ParseException):\n    def __init__(self, hdr, req=None):\n        self.hdr = hdr\n        self.req = req\n\n    def __str__(self):\n        return \"Invalid HTTP Header: %r\" % self.hdr\n\n\nclass ObsoleteFolding(ParseException):\n    def __init__(self, hdr):\n        self.hdr = hdr\n\n    def __str__(self):\n        return \"Obsolete line folding is unacceptable: %r\" % (self.hdr, )\n\n\nclass InvalidHeaderName(ParseException):\n    def __init__(self, hdr):\n        self.hdr = hdr\n\n    def __str__(self):\n        return \"Invalid HTTP header name: %r\" % self.hdr\n\n\nclass UnsupportedTransferCoding(ParseException):\n    def __init__(self, hdr):\n        self.hdr = hdr\n        self.code = 501\n\n    def __str__(self):\n        return \"Unsupported transfer coding: %r\" % self.hdr\n\n\nclass InvalidChunkSize(IOError):\n    def __init__(self, data):\n        self.data = data\n\n    def __str__(self):\n        return \"Invalid chunk size: %r\" % self.data\n\n\nclass ChunkMissingTerminator(IOError):\n    def __init__(self, term):\n        self.term = term\n\n    def __str__(self):\n        return \"Invalid chunk terminator is not '\\\\r\\\\n': %r\" % self.term\n\n\nclass LimitRequestLine(ParseException):\n    def __init__(self, size, max_size):\n        self.size = size\n        self.max_size = max_size\n\n    def __str__(self):\n        return \"Request Line is too large (%s > %s)\" % (self.size, self.max_size)\n\n\nclass LimitRequestHeaders(ParseException):\n    def __init__(self, msg):\n        self.msg = msg\n\n    def __str__(self):\n        return self.msg\n\n\nclass InvalidProxyLine(ParseException):\n    def __init__(self, line):\n        self.line = line\n        self.code = 400\n\n    def __str__(self):\n        return \"Invalid PROXY line: %r\" % self.line\n\n\nclass InvalidProxyHeader(ParseException):\n    def __init__(self, msg):\n        self.msg = msg\n        self.code = 400\n\n    def __str__(self):\n        return \"Invalid PROXY header: %s\" % self.msg\n\n\nclass ForbiddenProxyRequest(ParseException):\n    def __init__(self, host):\n        self.host = host\n        self.code = 403\n\n    def __str__(self):\n        return \"Proxy request from %r not allowed\" % self.host\n\n\nclass InvalidSchemeHeaders(ParseException):\n    def __str__(self):\n        return \"Contradictory scheme headers\"\n"
  },
  {
    "path": "gunicorn/http/message.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom enum import IntEnum\nimport ipaddress\nimport re\nimport socket\nimport struct\n\nfrom gunicorn.http.body import ChunkedReader, LengthReader, EOFReader, Body\nfrom gunicorn.http.errors import (\n    InvalidHeader, InvalidHeaderName, NoMoreData,\n    InvalidRequestLine, InvalidRequestMethod, InvalidHTTPVersion,\n    LimitRequestLine, LimitRequestHeaders,\n    UnsupportedTransferCoding, ObsoleteFolding,\n    ExpectationFailed,\n)\nfrom gunicorn.http.errors import InvalidProxyLine, InvalidProxyHeader, ForbiddenProxyRequest\nfrom gunicorn.http.errors import InvalidSchemeHeaders\nfrom gunicorn.util import bytes_to_str, split_request_uri\n\n\n# PROXY protocol v2 constants\nPP_V2_SIGNATURE = b\"\\x0D\\x0A\\x0D\\x0A\\x00\\x0D\\x0A\\x51\\x55\\x49\\x54\\x0A\"\n\n\nclass PPCommand(IntEnum):\n    \"\"\"PROXY protocol v2 commands.\"\"\"\n    LOCAL = 0x0\n    PROXY = 0x1\n\n\nclass PPFamily(IntEnum):\n    \"\"\"PROXY protocol v2 address families.\"\"\"\n    UNSPEC = 0x0\n    INET = 0x1   # IPv4\n    INET6 = 0x2  # IPv6\n    UNIX = 0x3\n\n\nclass PPProtocol(IntEnum):\n    \"\"\"PROXY protocol v2 transport protocols.\"\"\"\n    UNSPEC = 0x0\n    STREAM = 0x1  # TCP\n    DGRAM = 0x2   # UDP\n\n\nMAX_REQUEST_LINE = 8190\nMAX_HEADERS = 32768\nDEFAULT_MAX_HEADERFIELD_SIZE = 8190\n\n# verbosely on purpose, avoid backslash ambiguity\nRFC9110_5_6_2_TOKEN_SPECIALS = r\"!#$%&'*+-.^_`|~\"\nTOKEN_RE = re.compile(r\"[%s0-9a-zA-Z]+\" % (re.escape(RFC9110_5_6_2_TOKEN_SPECIALS)))\nMETHOD_BADCHAR_RE = re.compile(\"[a-z#]\")\n# usually 1.0 or 1.1 - RFC9112 permits restricting to single-digit versions\nVERSION_RE = re.compile(r\"HTTP/(\\d)\\.(\\d)\")\nRFC9110_5_5_INVALID_AND_DANGEROUS = re.compile(r\"[\\0\\r\\n]\")\n\n\ndef _ip_in_allow_list(ip_str, allow_list, networks):\n    \"\"\"Check if IP address is in the allow list.\n\n    Args:\n        ip_str: The IP address string to check\n        allow_list: The original allow list (strings, may contain \"*\")\n        networks: Pre-computed ipaddress.ip_network objects from config\n    \"\"\"\n    if '*' in allow_list:\n        return True\n    try:\n        ip = ipaddress.ip_address(ip_str)\n    except ValueError:\n        return False\n    for network in networks:\n        if ip in network:\n            return True\n    return False\n\n\nclass Message:\n    def __init__(self, cfg, unreader, peer_addr):\n        self.cfg = cfg\n        self.unreader = unreader\n        self.peer_addr = peer_addr\n        self.remote_addr = peer_addr\n        self.version = None\n        self.headers = []\n        self.trailers = []\n        self.body = None\n        self.scheme = \"https\" if cfg.is_ssl else \"http\"\n        self.must_close = False\n        self._expected_100_continue = False\n\n        # set headers limits\n        self.limit_request_fields = cfg.limit_request_fields\n        if (self.limit_request_fields <= 0\n                or self.limit_request_fields > MAX_HEADERS):\n            self.limit_request_fields = MAX_HEADERS\n        self.limit_request_field_size = cfg.limit_request_field_size\n        if self.limit_request_field_size < 0:\n            self.limit_request_field_size = DEFAULT_MAX_HEADERFIELD_SIZE\n\n        # set max header buffer size\n        max_header_field_size = self.limit_request_field_size or DEFAULT_MAX_HEADERFIELD_SIZE\n        self.max_buffer_headers = self.limit_request_fields * \\\n            (max_header_field_size + 2) + 4\n\n        unused = self.parse(self.unreader)\n        self.unreader.unread(unused)\n        self.set_body_reader()\n\n    def force_close(self):\n        self.must_close = True\n\n    def parse(self, unreader):\n        raise NotImplementedError()\n\n    def parse_headers(self, data, from_trailer=False):\n        cfg = self.cfg\n        headers = []\n\n        # Split lines on \\r\\n\n        lines = [bytes_to_str(line) for line in data.split(b\"\\r\\n\")]\n\n        # handle scheme headers\n        scheme_header = False\n        secure_scheme_headers = {}\n        forwarder_headers = []\n        if from_trailer:\n            # nonsense. either a request is https from the beginning\n            #  .. or we are just behind a proxy who does not remove conflicting trailers\n            pass\n        elif (not isinstance(self.peer_addr, tuple)\n              or _ip_in_allow_list(self.peer_addr[0], cfg.forwarded_allow_ips,\n                                   cfg.forwarded_allow_networks())):\n            secure_scheme_headers = cfg.secure_scheme_headers\n            forwarder_headers = cfg.forwarder_headers\n\n        # Parse headers into key/value pairs paying attention\n        # to continuation lines.\n        while lines:\n            if len(headers) >= self.limit_request_fields:\n                raise LimitRequestHeaders(\"limit request headers fields\")\n\n            # Parse initial header name: value pair.\n            curr = lines.pop(0)\n            header_length = len(curr) + len(\"\\r\\n\")\n            if curr.find(\":\") <= 0:\n                raise InvalidHeader(curr)\n            name, value = curr.split(\":\", 1)\n            if self.cfg.strip_header_spaces:\n                name = name.rstrip(\" \\t\")\n            if not TOKEN_RE.fullmatch(name):\n                raise InvalidHeaderName(name)\n\n            # this is still a dangerous place to do this\n            #  but it is more correct than doing it before the pattern match:\n            # after we entered Unicode wonderland, 8bits could case-shift into ASCII:\n            # b\"\\xDF\".decode(\"latin-1\").upper().encode(\"ascii\") == b\"SS\"\n            name = name.upper()\n\n            value = [value.strip(\" \\t\")]\n\n            # Consume value continuation lines..\n            while lines and lines[0].startswith((\" \", \"\\t\")):\n                # .. which is obsolete here, and no longer done by default\n                if not self.cfg.permit_obsolete_folding:\n                    raise ObsoleteFolding(name)\n                curr = lines.pop(0)\n                header_length += len(curr) + len(\"\\r\\n\")\n                if header_length > self.limit_request_field_size > 0:\n                    raise LimitRequestHeaders(\"limit request headers \"\n                                              \"fields size\")\n                value.append(curr.strip(\"\\t \"))\n            value = \" \".join(value)\n\n            if RFC9110_5_5_INVALID_AND_DANGEROUS.search(value):\n                raise InvalidHeader(name)\n\n            if header_length > self.limit_request_field_size > 0:\n                raise LimitRequestHeaders(\"limit request headers fields size\")\n\n            if not from_trailer and name == \"EXPECT\":\n                # https://datatracker.ietf.org/doc/html/rfc9110#section-10.1.1\n                # \"The Expect field value is case-insensitive.\"\n                if value.lower() == \"100-continue\":\n                    if self.version < (1, 1):\n                        # https://datatracker.ietf.org/doc/html/rfc9110#section-10.1.1-12\n                        # \"A server that receives a 100-continue expectation\n                        #  in an HTTP/1.0 request MUST ignore that expectation.\"\n                        pass\n                    else:\n                        self._expected_100_continue = True\n                    # N.B. understood but ignored expect header does not return 417\n                else:\n                    raise ExpectationFailed(value)\n\n            if name in secure_scheme_headers:\n                secure = value == secure_scheme_headers[name]\n                scheme = \"https\" if secure else \"http\"\n                if scheme_header:\n                    if scheme != self.scheme:\n                        raise InvalidSchemeHeaders()\n                else:\n                    scheme_header = True\n                    self.scheme = scheme\n\n            # ambiguous mapping allows fooling downstream, e.g. merging non-identical headers:\n            # X-Forwarded-For: 2001:db8::ha:cc:ed\n            # X_Forwarded_For: 127.0.0.1,::1\n            # HTTP_X_FORWARDED_FOR = 2001:db8::ha:cc:ed,127.0.0.1,::1\n            # Only modify after fixing *ALL* header transformations; network to wsgi env\n            if \"_\" in name:\n                if name in forwarder_headers or \"*\" in forwarder_headers:\n                    # This forwarder may override our environment\n                    pass\n                elif self.cfg.header_map == \"dangerous\":\n                    # as if we did not know we cannot safely map this\n                    pass\n                elif self.cfg.header_map == \"drop\":\n                    # almost as if it never had been there\n                    # but still counts against resource limits\n                    continue\n                else:\n                    # fail-safe fallthrough: refuse\n                    raise InvalidHeaderName(name)\n\n            headers.append((name, value))\n\n        return headers\n\n    def set_body_reader(self):\n        chunked = False\n        content_length = None\n\n        for (name, value) in self.headers:\n            if name == \"CONTENT-LENGTH\":\n                if content_length is not None:\n                    raise InvalidHeader(\"CONTENT-LENGTH\", req=self)\n                content_length = value\n            elif name == \"TRANSFER-ENCODING\":\n                # T-E can be a list\n                # https://datatracker.ietf.org/doc/html/rfc9112#name-transfer-encoding\n                vals = [v.strip() for v in value.split(',')]\n                for val in vals:\n                    if val.lower() == \"chunked\":\n                        # DANGER: transfer codings stack, and stacked chunking is never intended\n                        if chunked:\n                            raise InvalidHeader(\"TRANSFER-ENCODING\", req=self)\n                        chunked = True\n                    elif val.lower() == \"identity\":\n                        # does not do much, could still plausibly desync from what the proxy does\n                        # safe option: nuke it, its never needed\n                        if chunked:\n                            raise InvalidHeader(\"TRANSFER-ENCODING\", req=self)\n                    elif val.lower() in ('compress', 'deflate', 'gzip'):\n                        # chunked should be the last one\n                        if chunked:\n                            raise InvalidHeader(\"TRANSFER-ENCODING\", req=self)\n                        self.force_close()\n                    else:\n                        raise UnsupportedTransferCoding(value)\n\n        if chunked:\n            # two potentially dangerous cases:\n            #  a) CL + TE (TE overrides CL.. only safe if the recipient sees it that way too)\n            #  b) chunked HTTP/1.0 (always faulty)\n            if self.version < (1, 1):\n                # framing wonky, see RFC 9112 Section 6.1\n                raise InvalidHeader(\"TRANSFER-ENCODING\", req=self)\n            if content_length is not None:\n                # we cannot be certain the message framing we understood matches proxy intent\n                #  -> whatever happens next, remaining input must not be trusted\n                raise InvalidHeader(\"CONTENT-LENGTH\", req=self)\n            self.body = Body(ChunkedReader(self, self.unreader))\n        elif content_length is not None:\n            try:\n                if str(content_length).isnumeric():\n                    content_length = int(content_length)\n                else:\n                    raise InvalidHeader(\"CONTENT-LENGTH\", req=self)\n            except ValueError:\n                raise InvalidHeader(\"CONTENT-LENGTH\", req=self)\n\n            if content_length < 0:\n                raise InvalidHeader(\"CONTENT-LENGTH\", req=self)\n\n            self.body = Body(LengthReader(self.unreader, content_length))\n        else:\n            self.body = Body(EOFReader(self.unreader))\n\n    def should_close(self):\n        if self.must_close:\n            return True\n        for (h, v) in self.headers:\n            if h == \"CONNECTION\":\n                v = v.lower().strip(\" \\t\")\n                if v == \"close\":\n                    return True\n                elif v == \"keep-alive\":\n                    return False\n                break\n        return self.version <= (1, 0)\n\n\nclass Request(Message):\n    def __init__(self, cfg, unreader, peer_addr, req_number=1):\n        self.method = None\n        self.uri = None\n        self.path = None\n        self.query = None\n        self.fragment = None\n\n        # get max request line size\n        self.limit_request_line = cfg.limit_request_line\n        if (self.limit_request_line < 0\n                or self.limit_request_line >= MAX_REQUEST_LINE):\n            self.limit_request_line = MAX_REQUEST_LINE\n\n        self.req_number = req_number\n        self.proxy_protocol_info = None\n        super().__init__(cfg, unreader, peer_addr)\n\n    def get_data(self, unreader, buf, stop=False):\n        data = unreader.read()\n        if not data:\n            if stop:\n                raise StopIteration()\n            raise NoMoreData(buf.getvalue())\n        buf.write(data)\n\n    def parse(self, unreader):\n        buf = bytearray()\n        self.read_into(unreader, buf, stop=True)\n\n        # Handle proxy protocol if enabled and this is the first request\n        mode = self.cfg.proxy_protocol\n        if mode != \"off\" and self.req_number == 1:\n            buf = self._handle_proxy_protocol(unreader, buf, mode)\n\n        # Get request line\n        line, buf = self.read_line(unreader, buf, self.limit_request_line)\n\n        self.parse_request_line(line)\n\n        # Headers\n        data = bytes(buf)\n\n        done = data[:2] == b\"\\r\\n\"\n        while True:\n            idx = data.find(b\"\\r\\n\\r\\n\")\n            done = data[:2] == b\"\\r\\n\"\n\n            if idx < 0 and not done:\n                self.read_into(unreader, buf)\n                data = bytes(buf)\n                if len(data) > self.max_buffer_headers:\n                    raise LimitRequestHeaders(\"max buffer headers\")\n            else:\n                break\n\n        if done:\n            self.unreader.unread(data[2:])\n            return b\"\"\n\n        self.headers = self.parse_headers(data[:idx], from_trailer=False)\n\n        ret = data[idx + 4:]\n        return ret\n\n    def read_into(self, unreader, buf, stop=False):\n        \"\"\"Read data from unreader and append to bytearray buffer.\"\"\"\n        data = unreader.read()\n        if not data:\n            if stop:\n                raise StopIteration()\n            raise NoMoreData(bytes(buf))\n        buf.extend(data)\n\n    def read_line(self, unreader, buf, limit=0):\n        \"\"\"Read a line from buffer, returning (line, remaining_buffer).\"\"\"\n        data = bytes(buf)\n\n        while True:\n            idx = data.find(b\"\\r\\n\")\n            if idx >= 0:\n                # check if the request line is too large\n                if idx > limit > 0:\n                    raise LimitRequestLine(idx, limit)\n                break\n            if len(data) - 2 > limit > 0:\n                raise LimitRequestLine(len(data), limit)\n            self.read_into(unreader, buf)\n            data = bytes(buf)\n\n        return (data[:idx],  # request line,\n                bytearray(data[idx + 2:]))  # residue in the buffer, skip \\r\\n\n\n    def read_bytes(self, unreader, buf, count):\n        \"\"\"Read exactly count bytes from buffer/unreader.\"\"\"\n        while len(buf) < count:\n            self.read_into(unreader, buf)\n        return bytes(buf[:count]), bytearray(buf[count:])\n\n    def _handle_proxy_protocol(self, unreader, buf, mode):\n        \"\"\"Handle PROXY protocol detection and parsing.\n\n        Returns the buffer with proxy protocol data consumed.\n        \"\"\"\n        # Ensure we have enough data to detect v2 signature (12 bytes)\n        while len(buf) < 12:\n            self.read_into(unreader, buf)\n\n        # Check for v2 signature first\n        if mode in (\"v2\", \"auto\") and buf[:12] == PP_V2_SIGNATURE:\n            self.proxy_protocol_access_check()\n            return self._parse_proxy_protocol_v2(unreader, buf)\n\n        # Check for v1 prefix\n        if mode in (\"v1\", \"auto\") and buf[:6] == b\"PROXY \":\n            self.proxy_protocol_access_check()\n            return self._parse_proxy_protocol_v1(unreader, buf)\n\n        # Not proxy protocol - return buffer unchanged\n        return buf\n\n    def proxy_protocol_access_check(self):\n        \"\"\"Check if proxy protocol is allowed from this peer.\"\"\"\n        if (isinstance(self.peer_addr, tuple) and\n                not _ip_in_allow_list(self.peer_addr[0], self.cfg.proxy_allow_ips,\n                                      self.cfg.proxy_allow_networks())):\n            raise ForbiddenProxyRequest(self.peer_addr[0])\n\n    def _parse_proxy_protocol_v1(self, unreader, buf):\n        \"\"\"Parse PROXY protocol v1 (text format).\n\n        Returns buffer with v1 header consumed.\n        \"\"\"\n        # Read until we find \\r\\n\n        data = bytes(buf)\n        while b\"\\r\\n\" not in data:\n            self.read_into(unreader, buf)\n            data = bytes(buf)\n\n        idx = data.find(b\"\\r\\n\")\n        line = bytes_to_str(data[:idx])\n        remaining = bytearray(data[idx + 2:])\n\n        bits = line.split(\" \")\n\n        if len(bits) != 6:\n            raise InvalidProxyLine(line)\n\n        # Extract data\n        proto = bits[1]\n        s_addr = bits[2]\n        d_addr = bits[3]\n\n        # Validation\n        if proto not in [\"TCP4\", \"TCP6\"]:\n            raise InvalidProxyLine(\"protocol '%s' not supported\" % proto)\n        if proto == \"TCP4\":\n            try:\n                socket.inet_pton(socket.AF_INET, s_addr)\n                socket.inet_pton(socket.AF_INET, d_addr)\n            except OSError:\n                raise InvalidProxyLine(line)\n        elif proto == \"TCP6\":\n            try:\n                socket.inet_pton(socket.AF_INET6, s_addr)\n                socket.inet_pton(socket.AF_INET6, d_addr)\n            except OSError:\n                raise InvalidProxyLine(line)\n\n        try:\n            s_port = int(bits[4])\n            d_port = int(bits[5])\n        except ValueError:\n            raise InvalidProxyLine(\"invalid port %s\" % line)\n\n        if not ((0 <= s_port <= 65535) and (0 <= d_port <= 65535)):\n            raise InvalidProxyLine(\"invalid port %s\" % line)\n\n        # Set data\n        self.proxy_protocol_info = {\n            \"proxy_protocol\": proto,\n            \"client_addr\": s_addr,\n            \"client_port\": s_port,\n            \"proxy_addr\": d_addr,\n            \"proxy_port\": d_port\n        }\n\n        return remaining\n\n    def _parse_proxy_protocol_v2(self, unreader, buf):\n        \"\"\"Parse PROXY protocol v2 (binary format).\n\n        Returns buffer with v2 header consumed.\n        \"\"\"\n        # We need at least 16 bytes for the header (12 signature + 4 header)\n        while len(buf) < 16:\n            self.read_into(unreader, buf)\n\n        # Parse header fields (after 12-byte signature)\n        ver_cmd = buf[12]\n        fam_proto = buf[13]\n        length = struct.unpack(\">H\", bytes(buf[14:16]))[0]\n\n        # Validate version (high nibble must be 0x2)\n        version = (ver_cmd & 0xF0) >> 4\n        if version != 2:\n            raise InvalidProxyHeader(\"unsupported version %d\" % version)\n\n        # Extract command (low nibble)\n        command = ver_cmd & 0x0F\n        if command not in (PPCommand.LOCAL, PPCommand.PROXY):\n            raise InvalidProxyHeader(\"unsupported command %d\" % command)\n\n        # Ensure we have the complete header\n        total_header_size = 16 + length\n        while len(buf) < total_header_size:\n            self.read_into(unreader, buf)\n\n        # For LOCAL command, no address info is provided\n        if command == PPCommand.LOCAL:\n            self.proxy_protocol_info = {\n                \"proxy_protocol\": \"LOCAL\",\n                \"client_addr\": None,\n                \"client_port\": None,\n                \"proxy_addr\": None,\n                \"proxy_port\": None\n            }\n            return bytearray(buf[total_header_size:])\n\n        # Extract address family and protocol\n        family = (fam_proto & 0xF0) >> 4\n        protocol = fam_proto & 0x0F\n\n        # We only support TCP (STREAM)\n        if protocol != PPProtocol.STREAM:\n            raise InvalidProxyHeader(\"only TCP protocol is supported\")\n\n        addr_data = bytes(buf[16:16 + length])\n\n        if family == PPFamily.INET:  # IPv4\n            if length < 12:  # 4+4+2+2\n                raise InvalidProxyHeader(\"insufficient address data for IPv4\")\n            s_addr = socket.inet_ntop(socket.AF_INET, addr_data[0:4])\n            d_addr = socket.inet_ntop(socket.AF_INET, addr_data[4:8])\n            s_port = struct.unpack(\">H\", addr_data[8:10])[0]\n            d_port = struct.unpack(\">H\", addr_data[10:12])[0]\n            proto = \"TCP4\"\n\n        elif family == PPFamily.INET6:  # IPv6\n            if length < 36:  # 16+16+2+2\n                raise InvalidProxyHeader(\"insufficient address data for IPv6\")\n            s_addr = socket.inet_ntop(socket.AF_INET6, addr_data[0:16])\n            d_addr = socket.inet_ntop(socket.AF_INET6, addr_data[16:32])\n            s_port = struct.unpack(\">H\", addr_data[32:34])[0]\n            d_port = struct.unpack(\">H\", addr_data[34:36])[0]\n            proto = \"TCP6\"\n\n        elif family == PPFamily.UNSPEC:\n            # No address info provided with PROXY command\n            self.proxy_protocol_info = {\n                \"proxy_protocol\": \"UNSPEC\",\n                \"client_addr\": None,\n                \"client_port\": None,\n                \"proxy_addr\": None,\n                \"proxy_port\": None\n            }\n            return bytearray(buf[total_header_size:])\n\n        else:\n            raise InvalidProxyHeader(\"unsupported address family %d\" % family)\n\n        # Set data\n        self.proxy_protocol_info = {\n            \"proxy_protocol\": proto,\n            \"client_addr\": s_addr,\n            \"client_port\": s_port,\n            \"proxy_addr\": d_addr,\n            \"proxy_port\": d_port\n        }\n\n        return bytearray(buf[total_header_size:])\n\n    def parse_request_line(self, line_bytes):\n        bits = [bytes_to_str(bit) for bit in line_bytes.split(b\" \", 2)]\n        if len(bits) != 3:\n            raise InvalidRequestLine(bytes_to_str(line_bytes))\n\n        # Method: RFC9110 Section 9\n        self.method = bits[0]\n\n        # nonstandard restriction, suitable for all IANA registered methods\n        # partially enforced in previous gunicorn versions\n        if not self.cfg.permit_unconventional_http_method:\n            if METHOD_BADCHAR_RE.search(self.method):\n                raise InvalidRequestMethod(self.method)\n            if not 3 <= len(bits[0]) <= 20:\n                raise InvalidRequestMethod(self.method)\n        # standard restriction: RFC9110 token\n        if not TOKEN_RE.fullmatch(self.method):\n            raise InvalidRequestMethod(self.method)\n        # nonstandard and dangerous\n        # methods are merely uppercase by convention, no case-insensitive treatment is intended\n        if self.cfg.casefold_http_method:\n            self.method = self.method.upper()\n\n        # URI\n        self.uri = bits[1]\n\n        # Python stdlib explicitly tells us it will not perform validation.\n        # https://docs.python.org/3/library/urllib.parse.html#url-parsing-security\n        # There are *four* `request-target` forms in rfc9112, none of them can be empty:\n        # 1. origin-form, which starts with a slash\n        # 2. absolute-form, which starts with a non-empty scheme\n        # 3. authority-form, (for CONNECT) which contains a colon after the host\n        # 4. asterisk-form, which is an asterisk (`\\x2A`)\n        # => manually reject one always invalid URI: empty\n        if len(self.uri) == 0:\n            raise InvalidRequestLine(bytes_to_str(line_bytes))\n\n        try:\n            parts = split_request_uri(self.uri)\n        except ValueError:\n            raise InvalidRequestLine(bytes_to_str(line_bytes))\n        self.path = parts.path or \"\"\n        self.query = parts.query or \"\"\n        self.fragment = parts.fragment or \"\"\n\n        # Version\n        match = VERSION_RE.fullmatch(bits[2])\n        if match is None:\n            raise InvalidHTTPVersion(bits[2])\n        self.version = (int(match.group(1)), int(match.group(2)))\n        if not (1, 0) <= self.version < (2, 0):\n            # if ever relaxing this, carefully review Content-Encoding processing\n            if not self.cfg.permit_unconventional_http_version:\n                raise InvalidHTTPVersion(self.version)\n\n    def set_body_reader(self):\n        super().set_body_reader()\n        if isinstance(self.body.reader, EOFReader):\n            self.body = Body(LengthReader(self.unreader, 0))\n"
  },
  {
    "path": "gunicorn/http/parser.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport ssl\n\nfrom gunicorn.http.message import Request\nfrom gunicorn.http.unreader import SocketUnreader, IterUnreader\n\n\nclass Parser:\n\n    mesg_class = None\n\n    def __init__(self, cfg, source, source_addr):\n        self.cfg = cfg\n        if hasattr(source, \"recv\"):\n            self.unreader = SocketUnreader(source)\n        else:\n            self.unreader = IterUnreader(source)\n        self.mesg = None\n        self.source_addr = source_addr\n\n        # request counter (for keepalive connetions)\n        self.req_count = 0\n\n    def __iter__(self):\n        return self\n\n    def finish_body(self):\n        \"\"\"Discard any unread body of the current message.\n\n        This should be called before returning a keepalive connection to\n        the poller to ensure the socket doesn't appear readable due to\n        leftover body bytes.\n        \"\"\"\n        if self.mesg:\n            try:\n                data = self.mesg.body.read(1024)\n                while data:\n                    data = self.mesg.body.read(1024)\n            except ssl.SSLWantReadError:\n                # SSL socket has no more application data available\n                pass\n\n    def __next__(self):\n        # Stop if HTTP dictates a stop.\n        if self.mesg and self.mesg.should_close():\n            raise StopIteration()\n\n        # Discard any unread body of the previous message\n        self.finish_body()\n\n        # Parse the next request\n        self.req_count += 1\n        self.mesg = self.mesg_class(self.cfg, self.unreader, self.source_addr, self.req_count)\n        if not self.mesg:\n            raise StopIteration()\n        return self.mesg\n\n    next = __next__\n\n\nclass RequestParser(Parser):\n\n    mesg_class = Request\n"
  },
  {
    "path": "gunicorn/http/unreader.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport io\nimport os\n\n# Classes that can undo reading data from\n# a given type of data source.\n\n\nclass Unreader:\n    def __init__(self):\n        self.buf = io.BytesIO()\n\n    def chunk(self):\n        raise NotImplementedError()\n\n    def read(self, size=None):\n        if size is not None and not isinstance(size, int):\n            raise TypeError(\"size parameter must be an int or long.\")\n\n        if size is not None:\n            if size == 0:\n                return b\"\"\n            if size < 0:\n                size = None\n\n        self.buf.seek(0, os.SEEK_END)\n\n        if size is None and self.buf.tell():\n            ret = self.buf.getvalue()\n            self.buf = io.BytesIO()\n            return ret\n        if size is None:\n            d = self.chunk()\n            return d\n\n        while self.buf.tell() < size:\n            chunk = self.chunk()\n            if not chunk:\n                ret = self.buf.getvalue()\n                self.buf = io.BytesIO()\n                return ret\n            self.buf.write(chunk)\n        data = self.buf.getvalue()\n        self.buf = io.BytesIO()\n        self.buf.write(data[size:])\n        return data[:size]\n\n    def unread(self, data):\n        rest = self.buf.getvalue()\n        self.buf = io.BytesIO()\n        self.buf.write(data)\n        self.buf.write(rest)\n\n\nclass SocketUnreader(Unreader):\n    def __init__(self, sock, max_chunk=8192):\n        super().__init__()\n        self.sock = sock\n        self.mxchunk = max_chunk\n\n    def chunk(self):\n        return self.sock.recv(self.mxchunk)\n\n\nclass IterUnreader(Unreader):\n    def __init__(self, iterable):\n        super().__init__()\n        self.iter = iter(iterable)\n\n    def chunk(self):\n        if not self.iter:\n            return b\"\"\n        try:\n            return next(self.iter)\n        except StopIteration:\n            self.iter = None\n            return b\"\"\n"
  },
  {
    "path": "gunicorn/http/wsgi.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport io\nimport logging\nimport os\nimport re\nimport sys\n\nfrom gunicorn.http.message import TOKEN_RE\nfrom gunicorn.http.errors import ConfigurationProblem, InvalidHeader, InvalidHeaderName\nfrom gunicorn import SERVER_SOFTWARE, SERVER\nfrom gunicorn import util\n\n# Send files in at most 1GB blocks as some operating systems can have problems\n# with sending files in blocks over 2GB.\nBLKSIZE = 0x3FFFFFFF\n\n# RFC9110 5.5: field-vchar = VCHAR / obs-text\n# RFC4234 B.1: VCHAR = 0x21-x07E = printable ASCII\nHEADER_VALUE_RE = re.compile(r'[ \\t\\x21-\\x7e\\x80-\\xff]*')\n\nlog = logging.getLogger(__name__)\n\n\nclass FileWrapper:\n\n    def __init__(self, filelike, blksize=8192):\n        self.filelike = filelike\n        self.blksize = blksize\n        if hasattr(filelike, 'close'):\n            self.close = filelike.close\n\n    def __getitem__(self, key):\n        data = self.filelike.read(self.blksize)\n        if data:\n            return data\n        raise IndexError\n\n\nclass WSGIErrorsWrapper(io.RawIOBase):\n\n    def __init__(self, cfg):\n        # There is no public __init__ method for RawIOBase so\n        # we don't need to call super() in the __init__ method.\n        # pylint: disable=super-init-not-called\n        errorlog = logging.getLogger(\"gunicorn.error\")\n        handlers = errorlog.handlers\n        self.streams = []\n\n        if cfg.errorlog == \"-\":\n            self.streams.append(sys.stderr)\n            handlers = handlers[1:]\n\n        for h in handlers:\n            if hasattr(h, \"stream\"):\n                self.streams.append(h.stream)\n\n    def write(self, data):\n        for stream in self.streams:\n            try:\n                stream.write(data)\n            except UnicodeError:\n                stream.write(data.encode(\"UTF-8\"))\n            stream.flush()\n\n\ndef base_environ(cfg):\n    return {\n        \"wsgi.errors\": WSGIErrorsWrapper(cfg),\n        \"wsgi.version\": (1, 0),\n        \"wsgi.multithread\": False,\n        \"wsgi.multiprocess\": (cfg.workers > 1),\n        \"wsgi.run_once\": False,\n        \"wsgi.file_wrapper\": FileWrapper,\n        \"wsgi.input_terminated\": True,\n        \"SERVER_SOFTWARE\": SERVER_SOFTWARE,\n    }\n\n\ndef default_environ(req, sock, cfg):\n    env = base_environ(cfg)\n    env.update({\n        \"wsgi.input\": req.body,\n        \"gunicorn.socket\": sock,\n        \"REQUEST_METHOD\": req.method,\n        \"QUERY_STRING\": req.query,\n        \"RAW_URI\": req.uri,\n        \"SERVER_PROTOCOL\": \"HTTP/%s\" % \".\".join([str(v) for v in req.version])\n    })\n    return env\n\n\ndef proxy_environ(req):\n    info = req.proxy_protocol_info\n\n    if not info:\n        return {}\n\n    return {\n        \"PROXY_PROTOCOL\": info[\"proxy_protocol\"],\n        \"REMOTE_ADDR\": info[\"client_addr\"],\n        \"REMOTE_PORT\": str(info[\"client_port\"]),\n        \"PROXY_ADDR\": info[\"proxy_addr\"],\n        \"PROXY_PORT\": str(info[\"proxy_port\"]),\n    }\n\n\ndef _make_early_hints_callback(req, sock, resp):\n    \"\"\"Create a wsgi.early_hints callback for sending 103 Early Hints.\n\n    This allows WSGI applications to send 103 Early Hints responses\n    before the final response, enabling browsers to preload resources.\n\n    Args:\n        req: The request object\n        sock: The socket to write to\n        resp: The Response object to check if headers have been sent\n\n    Returns:\n        A callback function that accepts a list of (name, value) header tuples\n        and sends a 103 Early Hints response.\n\n    Note:\n        - Early hints are only sent for HTTP/1.1 or later clients\n        - HTTP/1.0 clients will silently ignore the callback\n        - Multiple calls are allowed (sending multiple 103 responses)\n        - Calls after response has started are silently ignored\n    \"\"\"\n    def send_early_hints(headers):\n        \"\"\"Send 103 Early Hints response.\n\n        Args:\n            headers: List of (name, value) header tuples, typically Link headers\n                     Example: [('Link', '</style.css>; rel=preload; as=style')]\n        \"\"\"\n        # Don't send after response has started - would break framing\n        if resp.headers_sent:\n            return\n\n        # Don't send to HTTP/1.0 clients - they don't support 1xx responses\n        if req.version < (1, 1):\n            return\n\n        # Build 103 response\n        response = b\"HTTP/1.1 103 Early Hints\\r\\n\"\n        for name, value in headers:\n            if isinstance(name, bytes):\n                name = name.decode('latin-1')\n            if isinstance(value, bytes):\n                value = value.decode('latin-1')\n            response += f\"{name}: {value}\\r\\n\".encode('latin-1')\n        response += b\"\\r\\n\"\n\n        util.write(sock, response)\n\n    return send_early_hints\n\n\ndef create(req, sock, client, server, cfg):\n    resp = Response(req, sock, cfg)\n\n    # set initial environ\n    environ = default_environ(req, sock, cfg)\n\n    # default variables\n    host = None\n    script_name = os.environ.get(\"SCRIPT_NAME\", \"\")\n\n    if req._expected_100_continue:\n        sock.send(b\"HTTP/1.1 100 Continue\\r\\n\\r\\n\")\n        # rfc9112: Expect MUST be forwarded if the request is forwarded\n        # N.B. gunicorn just sends at most one - application might send another\n\n    # add the headers to the environ\n    for hdr_name, hdr_value in req.headers:\n        if hdr_name == 'HOST':\n            host = hdr_value\n        elif hdr_name == \"SCRIPT_NAME\":\n            script_name = hdr_value\n        elif hdr_name == \"CONTENT-TYPE\":\n            environ['CONTENT_TYPE'] = hdr_value\n            continue\n        elif hdr_name == \"CONTENT-LENGTH\":\n            environ['CONTENT_LENGTH'] = hdr_value\n            continue\n\n        # do not change lightly, this is a common source of security problems\n        # RFC9110 Section 17.10 discourages ambiguous or incomplete mappings\n        key = 'HTTP_' + hdr_name.replace('-', '_')\n        if key in environ:\n            hdr_value = \"%s,%s\" % (environ[key], hdr_value)\n        environ[key] = hdr_value\n\n    # set the url scheme\n    environ['wsgi.url_scheme'] = req.scheme\n\n    # set the REMOTE_* keys in environ\n    # authors should be aware that REMOTE_HOST and REMOTE_ADDR\n    # may not qualify the remote addr:\n    # http://www.ietf.org/rfc/rfc3875\n    if isinstance(client, str):\n        environ['REMOTE_ADDR'] = client\n    elif isinstance(client, bytes):\n        environ['REMOTE_ADDR'] = client.decode()\n    else:\n        environ['REMOTE_ADDR'] = client[0]\n        environ['REMOTE_PORT'] = str(client[1])\n\n    # handle the SERVER_*\n    # Normally only the application should use the Host header but since the\n    # WSGI spec doesn't support unix sockets, we are using it to create\n    # viable SERVER_* if possible.\n    if isinstance(server, str):\n        server = server.split(\":\")\n        if len(server) == 1:\n            # unix socket\n            if host:\n                server = host.split(':')\n                if len(server) == 1:\n                    if req.scheme == \"http\":\n                        server.append(80)\n                    elif req.scheme == \"https\":\n                        server.append(443)\n                    else:\n                        server.append('')\n            else:\n                # no host header given which means that we are not behind a\n                # proxy, so append an empty port.\n                server.append('')\n    environ['SERVER_NAME'] = server[0]\n    environ['SERVER_PORT'] = str(server[1])\n\n    # set the path and script name\n    path_info = req.path\n    if script_name:\n        if not path_info.startswith(script_name):\n            raise ConfigurationProblem(\n                \"Request path %r does not start with SCRIPT_NAME %r\" %\n                (path_info, script_name))\n        path_info = path_info[len(script_name):]\n    environ['PATH_INFO'] = util.unquote_to_wsgi_str(path_info)\n    environ['SCRIPT_NAME'] = script_name\n\n    # override the environ with the correct remote and server address if\n    # we are behind a proxy using the proxy protocol.\n    environ.update(proxy_environ(req))\n\n    # Add wsgi.early_hints callback for sending 103 Early Hints\n    environ['wsgi.early_hints'] = _make_early_hints_callback(req, sock, resp)\n\n    # Add HTTP/2 stream priority if available\n    if hasattr(req, 'priority_weight'):\n        environ['gunicorn.http2.priority_weight'] = req.priority_weight\n        environ['gunicorn.http2.priority_depends_on'] = req.priority_depends_on\n\n    return resp, environ\n\n\nclass Response:\n\n    def __init__(self, req, sock, cfg):\n        self.req = req\n        self.sock = sock\n        self.version = SERVER\n        self.status = None\n        self.chunked = False\n        self.must_close = False\n        self.headers = []\n        self.headers_sent = False\n        self.response_length = None\n        self.sent = 0\n        self.upgrade = False\n        self.cfg = cfg\n\n    def force_close(self):\n        self.must_close = True\n\n    def should_close(self):\n        if self.must_close or self.req.should_close():\n            return True\n        if self.response_length is not None or self.chunked:\n            return False\n        if self.req.method == 'HEAD':\n            return False\n        if self.status_code < 200 or self.status_code in (204, 304):\n            return False\n        return True\n\n    def start_response(self, status, headers, exc_info=None):\n        if exc_info:\n            try:\n                if self.status and self.headers_sent:\n                    util.reraise(exc_info[0], exc_info[1], exc_info[2])\n            finally:\n                exc_info = None\n        elif self.status is not None:\n            raise AssertionError(\"Response headers already set!\")\n\n        self.status = status\n\n        # get the status code from the response here so we can use it to check\n        # the need for the connection header later without parsing the string\n        # each time.\n        try:\n            self.status_code = int(self.status.split()[0])\n        except ValueError:\n            self.status_code = None\n\n        self.process_headers(headers)\n        self.chunked = self.is_chunked()\n        return self.write\n\n    def process_headers(self, headers):\n        for name, value in headers:\n            if not isinstance(name, str):\n                raise TypeError('%r is not a string' % name)\n\n            if not TOKEN_RE.fullmatch(name):\n                raise InvalidHeaderName('%r' % name)\n\n            if not isinstance(value, str):\n                raise TypeError('%r is not a string' % value)\n\n            if not HEADER_VALUE_RE.fullmatch(value):\n                raise InvalidHeader('%r' % value)\n\n            # RFC9110 5.5\n            value = value.strip(\" \\t\")\n            lname = name.lower()\n            if lname == \"content-length\":\n                self.response_length = int(value)\n            elif util.is_hoppish(name):\n                if lname == \"connection\":\n                    # handle websocket\n                    if value.lower() == \"upgrade\":\n                        self.upgrade = True\n                elif lname == \"upgrade\":\n                    if value.lower() == \"websocket\":\n                        self.headers.append((name, value))\n\n                # ignore hopbyhop headers\n                continue\n            self.headers.append((name, value))\n\n    def is_chunked(self):\n        # Only use chunked responses when the client is\n        # speaking HTTP/1.1 or newer and there was\n        # no Content-Length header set.\n        if self.response_length is not None:\n            return False\n        elif self.req.version <= (1, 0):\n            return False\n        elif self.req.method == 'HEAD':\n            # Responses to a HEAD request MUST NOT contain a response body.\n            return False\n        elif self.status_code in (204, 304):\n            # Do not use chunked responses when the response is guaranteed to\n            # not have a response body.\n            return False\n        return True\n\n    def default_headers(self):\n        # set the connection header\n        if self.upgrade:\n            connection = \"upgrade\"\n        elif self.should_close():\n            connection = \"close\"\n        else:\n            connection = \"keep-alive\"\n\n        headers = [\n            \"HTTP/%s.%s %s\\r\\n\" % (self.req.version[0],\n                                   self.req.version[1], self.status),\n            \"Server: %s\\r\\n\" % self.version,\n            \"Date: %s\\r\\n\" % util.http_date(),\n            \"Connection: %s\\r\\n\" % connection\n        ]\n        if self.chunked:\n            headers.append(\"Transfer-Encoding: chunked\\r\\n\")\n        return headers\n\n    def send_headers(self):\n        if self.headers_sent:\n            return\n        tosend = self.default_headers()\n        tosend.extend([\"%s: %s\\r\\n\" % (k, v) for k, v in self.headers])\n\n        header_str = \"%s\\r\\n\" % \"\".join(tosend)\n        util.write(self.sock, util.to_bytestring(header_str, \"latin-1\"))\n        self.headers_sent = True\n\n    def write(self, arg):\n        self.send_headers()\n        if not isinstance(arg, bytes):\n            raise TypeError('%r is not a byte' % arg)\n        arglen = len(arg)\n        tosend = arglen\n        if self.response_length is not None:\n            if self.sent >= self.response_length:\n                # Never write more than self.response_length bytes\n                return\n\n            tosend = min(self.response_length - self.sent, tosend)\n            if tosend < arglen:\n                arg = arg[:tosend]\n\n        # Sending an empty chunk signals the end of the\n        # response and prematurely closes the response\n        if self.chunked and tosend == 0:\n            return\n\n        self.sent += tosend\n        util.write(self.sock, arg, self.chunked)\n\n    def can_sendfile(self):\n        return self.cfg.sendfile is not False\n\n    def sendfile(self, respiter):\n        if self.cfg.is_ssl or not self.can_sendfile():\n            return False\n\n        if not util.has_fileno(respiter.filelike):\n            return False\n\n        fileno = respiter.filelike.fileno()\n        try:\n            offset = os.lseek(fileno, 0, os.SEEK_CUR)\n            if self.response_length is None:\n                filesize = os.fstat(fileno).st_size\n                nbytes = filesize - offset\n            else:\n                nbytes = self.response_length\n        except (OSError, io.UnsupportedOperation):\n            return False\n\n        self.send_headers()\n\n        if self.is_chunked():\n            chunk_size = \"%X\\r\\n\" % nbytes\n            self.sock.sendall(chunk_size.encode('utf-8'))\n        if nbytes > 0:\n            self.sock.sendfile(respiter.filelike, offset=offset, count=nbytes)\n\n        if self.is_chunked():\n            self.sock.sendall(b\"\\r\\n\")\n\n        os.lseek(fileno, offset, os.SEEK_SET)\n\n        return True\n\n    def write_file(self, respiter):\n        if not self.sendfile(respiter):\n            for item in respiter:\n                self.write(item)\n\n    def close(self):\n        if not self.headers_sent:\n            self.send_headers()\n        if self.chunked:\n            util.write_chunk(self.sock, b\"\")\n"
  },
  {
    "path": "gunicorn/http2/__init__.py",
    "content": "# -*- coding: utf-8 -\n#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nHTTP/2 support for Gunicorn.\n\nThis module provides HTTP/2 protocol support using the hyper-h2 library.\nHTTP/2 requires TLS with ALPN negotiation.\n\"\"\"\n\nH2_MIN_VERSION = (4, 1, 0)\n\n_h2_available = None\n_h2_version = None\n\n\ndef is_http2_available():\n    \"\"\"Check if HTTP/2 support is available.\n\n    Returns:\n        bool: True if the h2 library is installed with minimum required version.\n    \"\"\"\n    global _h2_available, _h2_version  # pylint: disable=global-statement\n\n    if _h2_available is not None:\n        return _h2_available\n\n    try:\n        import h2\n        version_str = getattr(h2, '__version__', '0.0.0')\n        version_parts = tuple(int(x) for x in version_str.split('.')[:3])\n        _h2_version = version_parts\n        _h2_available = version_parts >= H2_MIN_VERSION\n    except ImportError:\n        _h2_available = False\n        _h2_version = None\n\n    return _h2_available\n\n\ndef get_h2_version():\n    \"\"\"Get the installed h2 library version.\n\n    Returns:\n        tuple: Version tuple (major, minor, patch) or None if not installed.\n    \"\"\"\n    if _h2_version is None:\n        is_http2_available()  # Populate _h2_version\n    return _h2_version\n\n\ndef get_http2_connection_class():\n    \"\"\"Get the HTTP2ServerConnection class if h2 is available.\n\n    Returns:\n        HTTP2ServerConnection class, or raises HTTP2NotAvailable\n    \"\"\"\n    if not is_http2_available():\n        from .errors import HTTP2NotAvailable\n        raise HTTP2NotAvailable()\n    from .connection import HTTP2ServerConnection\n    return HTTP2ServerConnection\n\n\ndef get_async_http2_connection_class():\n    \"\"\"Get the AsyncHTTP2Connection class if h2 is available.\n\n    Returns:\n        AsyncHTTP2Connection class, or raises HTTP2NotAvailable\n    \"\"\"\n    if not is_http2_available():\n        from .errors import HTTP2NotAvailable\n        raise HTTP2NotAvailable()\n    from .async_connection import AsyncHTTP2Connection\n    return AsyncHTTP2Connection\n\n\n__all__ = [\n    'is_http2_available',\n    'get_h2_version',\n    'get_http2_connection_class',\n    'get_async_http2_connection_class',\n    'H2_MIN_VERSION',\n]\n"
  },
  {
    "path": "gunicorn/http2/async_connection.py",
    "content": "# -*- coding: utf-8 -\n#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nAsync HTTP/2 server connection implementation for ASGI workers.\n\nUses the hyper-h2 library for HTTP/2 protocol handling with\nasyncio for non-blocking I/O.\n\"\"\"\n\nimport asyncio\n\nfrom .errors import (\n    HTTP2Error, HTTP2ProtocolError, HTTP2ConnectionError,\n    HTTP2NotAvailable, HTTP2ErrorCode,\n)\nfrom .stream import HTTP2Stream\nfrom .request import HTTP2Request\n\n\n# Import h2 lazily to allow graceful fallback\n_h2 = None\n_h2_config = None\n_h2_events = None\n_h2_exceptions = None\n_h2_settings = None\n\n\ndef _import_h2():\n    \"\"\"Lazily import h2 library components.\"\"\"\n    global _h2, _h2_config, _h2_events, _h2_exceptions, _h2_settings  # pylint: disable=global-statement\n\n    if _h2 is not None:\n        return\n\n    try:\n        import h2.connection as _h2\n        import h2.config as _h2_config\n        import h2.events as _h2_events\n        import h2.exceptions as _h2_exceptions\n        import h2.settings as _h2_settings\n    except ImportError:\n        raise HTTP2NotAvailable()\n\n\nclass AsyncHTTP2Connection:\n    \"\"\"Async HTTP/2 server-side connection handler for ASGI.\n\n    Manages the HTTP/2 connection state and multiplexed streams\n    using asyncio for non-blocking I/O operations.\n    \"\"\"\n\n    # Default buffer size for socket reads\n    READ_BUFFER_SIZE = 65536\n\n    def __init__(self, cfg, reader, writer, client_addr):\n        \"\"\"Initialize an async HTTP/2 server connection.\n\n        Args:\n            cfg: Gunicorn configuration object\n            reader: asyncio StreamReader\n            writer: asyncio StreamWriter\n            client_addr: Client address tuple (host, port)\n\n        Raises:\n            HTTP2NotAvailable: If h2 library is not installed\n        \"\"\"\n        _import_h2()\n\n        self.cfg = cfg\n        self.reader = reader\n        self.writer = writer\n        self.client_addr = client_addr\n\n        # Active streams indexed by stream ID\n        self.streams = {}\n\n        # Queue of completed requests for the worker\n        self._request_queue = asyncio.Queue()\n\n        # Connection settings from config\n        self.initial_window_size = cfg.http2_initial_window_size\n        self.max_concurrent_streams = cfg.http2_max_concurrent_streams\n        self.max_frame_size = cfg.http2_max_frame_size\n        self.max_header_list_size = cfg.http2_max_header_list_size\n\n        # Initialize h2 connection\n        config = _h2_config.H2Configuration(\n            client_side=False,\n            header_encoding='utf-8',\n        )\n        self.h2_conn = _h2.H2Connection(config=config)\n\n        # Connection state\n        self._closed = False\n        self._initialized = False\n        self._receive_task = None\n\n    async def initiate_connection(self):\n        \"\"\"Send initial HTTP/2 settings to client.\n\n        Should be called after the SSL handshake completes and\n        before processing any data.\n        \"\"\"\n        if self._initialized:\n            return\n\n        # Update local settings before initiating\n        self.h2_conn.update_settings({\n            _h2_settings.SettingCodes.MAX_CONCURRENT_STREAMS: self.max_concurrent_streams,\n            _h2_settings.SettingCodes.INITIAL_WINDOW_SIZE: self.initial_window_size,\n            _h2_settings.SettingCodes.MAX_FRAME_SIZE: self.max_frame_size,\n            _h2_settings.SettingCodes.MAX_HEADER_LIST_SIZE: self.max_header_list_size,\n        })\n\n        self.h2_conn.initiate_connection()\n        await self._send_pending_data()\n        self._initialized = True\n\n    async def receive_data(self, timeout=None):\n        \"\"\"Receive data and return completed requests.\n\n        Args:\n            timeout: Optional timeout in seconds for read operation\n\n        Returns:\n            list: List of HTTP2Request objects for completed requests\n\n        Raises:\n            HTTP2ConnectionError: On protocol or connection errors\n            asyncio.TimeoutError: If timeout expires\n        \"\"\"\n        try:\n            if timeout is not None:\n                data = await asyncio.wait_for(\n                    self.reader.read(self.READ_BUFFER_SIZE),\n                    timeout=timeout\n                )\n            else:\n                data = await self.reader.read(self.READ_BUFFER_SIZE)\n        except (OSError, IOError) as e:\n            raise HTTP2ConnectionError(f\"Socket read error: {e}\")\n\n        if not data:\n            # Connection closed by peer\n            self._closed = True\n            return []\n\n        # Feed data to h2\n        # Note: Specific exceptions must come before ProtocolError (their parent class)\n        try:\n            events = self.h2_conn.receive_data(data)\n        except _h2_exceptions.FlowControlError as e:\n            # Send GOAWAY with FLOW_CONTROL_ERROR\n            await self.close(error_code=HTTP2ErrorCode.FLOW_CONTROL_ERROR)\n            raise HTTP2ProtocolError(str(e))\n        except _h2_exceptions.FrameTooLargeError as e:\n            # Send GOAWAY with FRAME_SIZE_ERROR\n            await self.close(error_code=HTTP2ErrorCode.FRAME_SIZE_ERROR)\n            raise HTTP2ProtocolError(str(e))\n        except _h2_exceptions.InvalidSettingsValueError as e:\n            # Use error_code from h2 exception (RFC 7540 Section 6.5.2):\n            # INITIAL_WINDOW_SIZE > 2^31-1 gives FLOW_CONTROL_ERROR\n            # Other invalid settings give PROTOCOL_ERROR\n            error_code = getattr(e, 'error_code', None)\n            if error_code is not None:\n                await self.close(error_code=error_code)\n            else:\n                await self.close(error_code=HTTP2ErrorCode.PROTOCOL_ERROR)\n            raise HTTP2ProtocolError(str(e))\n        except _h2_exceptions.TooManyStreamsError as e:\n            # Send GOAWAY with REFUSED_STREAM\n            await self.close(error_code=HTTP2ErrorCode.REFUSED_STREAM)\n            raise HTTP2ProtocolError(str(e))\n        except _h2_exceptions.ProtocolError as e:\n            # Send GOAWAY with PROTOCOL_ERROR before raising\n            await self.close(error_code=HTTP2ErrorCode.PROTOCOL_ERROR)\n            raise HTTP2ProtocolError(str(e))\n\n        # Process events\n        completed_requests = []\n        for event in events:\n            request = self._handle_event(event)\n            if request is not None:\n                completed_requests.append(request)\n\n        # Send any pending data (WINDOW_UPDATE, etc.)\n        await self._send_pending_data()\n\n        return completed_requests\n\n    def _handle_event(self, event):\n        \"\"\"Handle a single h2 event.\n\n        Args:\n            event: h2 event object\n\n        Returns:\n            HTTP2Request if a request is complete, None otherwise\n        \"\"\"\n        if isinstance(event, _h2_events.RequestReceived):\n            return self._handle_request_received(event)\n\n        elif isinstance(event, _h2_events.DataReceived):\n            return self._handle_data_received(event)\n\n        elif isinstance(event, _h2_events.StreamEnded):\n            return self._handle_stream_ended(event)\n\n        elif isinstance(event, _h2_events.StreamReset):\n            self._handle_stream_reset(event)\n\n        elif isinstance(event, _h2_events.WindowUpdated):\n            pass  # Flow control update, handled by h2\n\n        elif isinstance(event, _h2_events.PriorityUpdated):\n            self._handle_priority_updated(event)\n\n        elif isinstance(event, _h2_events.SettingsAcknowledged):\n            pass  # Settings ACK received\n\n        elif isinstance(event, _h2_events.ConnectionTerminated):\n            self._handle_connection_terminated(event)\n\n        elif isinstance(event, _h2_events.TrailersReceived):\n            return self._handle_trailers_received(event)\n\n        return None\n\n    def _handle_request_received(self, event):\n        \"\"\"Handle RequestReceived event (HEADERS frame).\"\"\"\n        stream_id = event.stream_id\n        headers = event.headers\n\n        # Create new stream\n        stream = HTTP2Stream(stream_id, self)\n        self.streams[stream_id] = stream\n\n        # Process headers\n        stream.receive_headers(headers, end_stream=False)\n\n    def _handle_data_received(self, event):\n        \"\"\"Handle DataReceived event.\"\"\"\n        stream_id = event.stream_id\n        data = event.data\n\n        stream = self.streams.get(stream_id)\n        if stream is None:\n            return None\n\n        stream.receive_data(data, end_stream=False)\n\n        # Increment flow control windows (only if data received)\n        if len(data) > 0:\n            try:\n                # Update stream-level window\n                self.h2_conn.increment_flow_control_window(len(data), stream_id=stream_id)\n                # Update connection-level window\n                self.h2_conn.increment_flow_control_window(len(data), stream_id=None)\n            except (ValueError, _h2_exceptions.FlowControlError):\n                # Window overflow - prepare GOAWAY with FLOW_CONTROL_ERROR\n                # (will be sent by receive_data's _send_pending_data call)\n                self._closed = True\n                try:\n                    self.h2_conn.close_connection(error_code=HTTP2ErrorCode.FLOW_CONTROL_ERROR)\n                except Exception:\n                    pass\n\n        return None\n\n    def _handle_stream_ended(self, event):\n        \"\"\"Handle StreamEnded event.\"\"\"\n        stream_id = event.stream_id\n        stream = self.streams.get(stream_id)\n\n        if stream is None:\n            return None\n\n        stream.request_complete = True\n        return HTTP2Request(stream, self.cfg, self.client_addr)\n\n    def _handle_stream_reset(self, event):\n        \"\"\"Handle StreamReset event.\"\"\"\n        stream_id = event.stream_id\n        stream = self.streams.get(stream_id)\n\n        if stream is not None:\n            stream.reset(event.error_code)\n\n    def _handle_connection_terminated(self, event):\n        \"\"\"Handle ConnectionTerminated event.\"\"\"\n        self._closed = True\n\n    def _handle_trailers_received(self, event):\n        \"\"\"Handle TrailersReceived event.\"\"\"\n        stream_id = event.stream_id\n        stream = self.streams.get(stream_id)\n\n        if stream is None:\n            return None\n\n        stream.receive_trailers(event.headers)\n        return HTTP2Request(stream, self.cfg, self.client_addr)\n\n    def _handle_priority_updated(self, event):\n        \"\"\"Handle PriorityUpdated event (PRIORITY frame).\n\n        Args:\n            event: PriorityUpdated event with priority info\n        \"\"\"\n        stream = self.streams.get(event.stream_id)\n        if stream is not None:\n            stream.update_priority(\n                weight=event.weight,\n                depends_on=event.depends_on,\n                exclusive=event.exclusive\n            )\n\n    async def send_informational(self, stream_id, status, headers):\n        \"\"\"Send an informational response (1xx) on a stream.\n\n        This is used for 103 Early Hints and other 1xx responses.\n        Informational responses are sent before the final response\n        and do not end the stream.\n\n        Args:\n            stream_id: The stream ID\n            status: HTTP status code (100-199)\n            headers: List of (name, value) header tuples\n\n        Raises:\n            HTTP2Error: If status is not in 1xx range\n        \"\"\"\n        if status < 100 or status >= 200:\n            raise HTTP2Error(f\"Invalid informational status: {status}\")\n\n        stream = self.streams.get(stream_id)\n        if stream is None:\n            raise HTTP2Error(f\"Stream {stream_id} not found\")\n\n        # Build headers with :status pseudo-header\n        response_headers = [(':status', str(status))]\n        for name, value in headers:\n            # HTTP/2 headers must be lowercase\n            response_headers.append((name.lower(), str(value)))\n\n        # Send headers with end_stream=False (informational, more to follow)\n        self.h2_conn.send_headers(stream_id, response_headers, end_stream=False)\n        await self._send_pending_data()\n\n    async def send_response(self, stream_id, status, headers, body=None):\n        \"\"\"Send a response on a stream.\n\n        Args:\n            stream_id: The stream ID to respond on\n            status: HTTP status code (int)\n            headers: List of (name, value) header tuples\n            body: Optional response body bytes\n\n        Returns:\n            bool: True if response sent, False if stream was already closed\n        \"\"\"\n        stream = self.streams.get(stream_id)\n        if stream is None:\n            # Stream was already cleaned up (reset/closed) - return gracefully\n            return False\n\n        # Build response headers with :status pseudo-header\n        response_headers = [(':status', str(status))]\n        for name, value in headers:\n            response_headers.append((name.lower(), str(value)))\n\n        end_stream = body is None or len(body) == 0\n\n        try:\n            # Send headers\n            self.h2_conn.send_headers(stream_id, response_headers, end_stream=end_stream)\n            stream.send_headers(response_headers, end_stream=end_stream)\n            await self._send_pending_data()\n\n            # Send body if present\n            if body and len(body) > 0:\n                await self.send_data(stream_id, body, end_stream=True)\n            return True\n        except _h2_exceptions.StreamClosedError:\n            # Stream was reset by client - clean up gracefully\n            stream.close()\n            self.cleanup_stream(stream_id)\n            return False\n\n    async def _wait_for_flow_control_window(self, stream_id):\n        \"\"\"Wait for flow control window to become positive.\n\n        Returns:\n            int: Available window size, or -1 if waiting failed\n        \"\"\"\n        max_wait_attempts = 50  # ~5 seconds at 100ms per attempt\n        for _ in range(max_wait_attempts):\n            available = self.h2_conn.local_flow_control_window(stream_id)\n            if available > 0:\n                return available\n\n            # Read more data from connection (may receive WINDOW_UPDATE)\n            try:\n                incoming = await asyncio.wait_for(\n                    self.reader.read(self.READ_BUFFER_SIZE),\n                    timeout=0.1\n                )\n                if incoming:\n                    events = self.h2_conn.receive_data(incoming)\n                    # Process events but don't create new requests\n                    for event in events:\n                        if isinstance(event, _h2_events.StreamReset):\n                            if event.stream_id == stream_id:\n                                return -1\n                        elif isinstance(event, _h2_events.ConnectionTerminated):\n                            self._closed = True\n                            return -1\n                    await self._send_pending_data()\n                else:\n                    # Connection closed\n                    self._closed = True\n                    return -1\n            except asyncio.TimeoutError:\n                continue\n            except _h2_exceptions.ProtocolError:\n                return -1\n\n        return self.h2_conn.local_flow_control_window(stream_id)\n\n    async def send_data(self, stream_id, data, end_stream=False):\n        \"\"\"Send data on a stream.\n\n        Args:\n            stream_id: The stream ID\n            data: Body data bytes\n            end_stream: Whether this ends the stream\n\n        Returns:\n            bool: True if data sent, False if stream was already closed\n        \"\"\"\n        stream = self.streams.get(stream_id)\n        if stream is None:\n            return False\n\n        data_to_send = data\n        try:\n            while data_to_send:\n                available = self.h2_conn.local_flow_control_window(stream_id)\n                chunk_size = min(available, self.max_frame_size, len(data_to_send))\n\n                if chunk_size <= 0:\n                    # Wait for WINDOW_UPDATE per RFC 7540 Section 6.9.2\n                    await self._send_pending_data()\n                    available = await self._wait_for_flow_control_window(stream_id)\n                    if available <= 0:\n                        return False\n                    chunk_size = min(available, self.max_frame_size, len(data_to_send))\n\n                chunk = data_to_send[:chunk_size]\n                data_to_send = data_to_send[chunk_size:]\n                is_final = end_stream and len(data_to_send) == 0\n\n                self.h2_conn.send_data(stream_id, chunk, end_stream=is_final)\n                await self._send_pending_data()\n\n            stream.send_data(data, end_stream=end_stream)\n            return True\n        except (_h2_exceptions.StreamClosedError, _h2_exceptions.FlowControlError):\n            stream.close()\n            self.cleanup_stream(stream_id)\n            return False\n\n    async def send_trailers(self, stream_id, trailers):\n        \"\"\"Send trailing headers on a stream.\n\n        Trailers are headers sent after the response body, commonly used\n        for gRPC status codes, checksums, and timing information.\n\n        Args:\n            stream_id: The stream ID\n            trailers: List of (name, value) trailer tuples\n\n        Raises:\n            HTTP2Error: If stream not found, headers not sent, or pseudo-headers used\n\n        Returns:\n            bool: True if trailers sent, False if stream was already closed\n        \"\"\"\n        stream = self.streams.get(stream_id)\n        if stream is None:\n            # Stream was already cleaned up (reset/closed) - return gracefully\n            return False\n        if not stream.response_headers_sent:\n            # Can't send trailers without headers - return False\n            return False\n\n        # Validate and normalize trailer headers\n        trailer_headers = []\n        for name, value in trailers:\n            lname = name.lower()\n            if lname.startswith(':'):\n                raise HTTP2Error(f\"Pseudo-header '{name}' not allowed in trailers\")\n            trailer_headers.append((lname, str(value)))\n\n        try:\n            # Send trailers with end_stream=True\n            self.h2_conn.send_headers(stream_id, trailer_headers, end_stream=True)\n            stream.send_trailers(trailer_headers)\n            await self._send_pending_data()\n            return True\n        except _h2_exceptions.StreamClosedError:\n            # Stream was reset by client - clean up gracefully\n            stream.close()\n            self.cleanup_stream(stream_id)\n            return False\n\n    async def send_error(self, stream_id, status_code, message=None):\n        \"\"\"Send an error response on a stream.\"\"\"\n        body = message.encode() if message else b''\n        headers = [('content-length', str(len(body)))]\n        if body:\n            headers.append(('content-type', 'text/plain; charset=utf-8'))\n\n        await self.send_response(stream_id, status_code, headers, body)\n\n    async def reset_stream(self, stream_id, error_code=0x8):\n        \"\"\"Reset a stream with RST_STREAM.\"\"\"\n        stream = self.streams.get(stream_id)\n        if stream is not None:\n            stream.reset(error_code)\n\n        self.h2_conn.reset_stream(stream_id, error_code=error_code)\n        await self._send_pending_data()\n\n    async def close(self, error_code=0x0, last_stream_id=None):\n        \"\"\"Close the connection gracefully with GOAWAY.\"\"\"\n        if self._closed:\n            return\n\n        self._closed = True\n\n        if last_stream_id is None:\n            last_stream_id = max(self.streams.keys()) if self.streams else 0\n\n        try:\n            self.h2_conn.close_connection(error_code=error_code)\n            await self._send_pending_data()\n        except Exception:\n            pass\n\n        try:\n            self.writer.close()\n            await self.writer.wait_closed()\n        except Exception:\n            pass\n\n    async def _send_pending_data(self):\n        \"\"\"Send any pending data from h2 to the socket.\"\"\"\n        data = self.h2_conn.data_to_send()\n        if data:\n            try:\n                self.writer.write(data)\n                await self.writer.drain()\n            except (OSError, IOError) as e:\n                self._closed = True\n                raise HTTP2ConnectionError(f\"Socket write error: {e}\")\n\n    @property\n    def is_closed(self):\n        \"\"\"Check if connection is closed.\"\"\"\n        return self._closed\n\n    def cleanup_stream(self, stream_id):\n        \"\"\"Remove a stream after processing is complete.\"\"\"\n        self.streams.pop(stream_id, None)\n\n    def __repr__(self):\n        return (\n            f\"<AsyncHTTP2Connection \"\n            f\"streams={len(self.streams)} \"\n            f\"closed={self._closed}>\"\n        )\n\n\n__all__ = ['AsyncHTTP2Connection']\n"
  },
  {
    "path": "gunicorn/http2/connection.py",
    "content": "# -*- coding: utf-8 -\n#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nHTTP/2 server connection implementation.\n\nUses the hyper-h2 library for HTTP/2 protocol handling.\n\"\"\"\n\nfrom io import BytesIO\n\nfrom .errors import (\n    HTTP2Error, HTTP2ProtocolError, HTTP2ConnectionError,\n    HTTP2NotAvailable, HTTP2ErrorCode,\n)\nfrom .stream import HTTP2Stream\nfrom .request import HTTP2Request\n\n\n# Import h2 lazily to allow graceful fallback\n_h2 = None\n_h2_config = None\n_h2_events = None\n_h2_exceptions = None\n_h2_settings = None\n\n\ndef _import_h2():\n    \"\"\"Lazily import h2 library components.\"\"\"\n    global _h2, _h2_config, _h2_events, _h2_exceptions, _h2_settings  # pylint: disable=global-statement\n\n    if _h2 is not None:\n        return\n\n    try:\n        import h2.connection as _h2\n        import h2.config as _h2_config\n        import h2.events as _h2_events\n        import h2.exceptions as _h2_exceptions\n        import h2.settings as _h2_settings\n    except ImportError:\n        raise HTTP2NotAvailable()\n\n\nclass HTTP2ServerConnection:\n    \"\"\"HTTP/2 server-side connection handler.\n\n    Manages the HTTP/2 connection state and multiplexed streams.\n    This class wraps the h2 library and provides a higher-level\n    interface for gunicorn workers.\n    \"\"\"\n\n    # Default buffer size for socket reads\n    READ_BUFFER_SIZE = 65536\n\n    def __init__(self, cfg, sock, client_addr):\n        \"\"\"Initialize an HTTP/2 server connection.\n\n        Args:\n            cfg: Gunicorn configuration object\n            sock: SSL socket with completed handshake\n            client_addr: Client address tuple (host, port)\n\n        Raises:\n            HTTP2NotAvailable: If h2 library is not installed\n        \"\"\"\n        _import_h2()\n\n        self.cfg = cfg\n        self.sock = sock\n        self.client_addr = client_addr\n\n        # Active streams indexed by stream ID\n        self.streams = {}\n\n        # Completed requests ready for processing\n        self._pending_requests = []\n\n        # Connection settings from config\n        self.initial_window_size = cfg.http2_initial_window_size\n        self.max_concurrent_streams = cfg.http2_max_concurrent_streams\n        self.max_frame_size = cfg.http2_max_frame_size\n        self.max_header_list_size = cfg.http2_max_header_list_size\n\n        # Initialize h2 connection\n        config = _h2_config.H2Configuration(\n            client_side=False,\n            header_encoding='utf-8',\n        )\n        self.h2_conn = _h2.H2Connection(config=config)\n\n        # Read buffer for partial frames\n        self._read_buffer = BytesIO()\n\n        # Connection state\n        self._closed = False\n        self._initialized = False\n\n    def initiate_connection(self):\n        \"\"\"Send initial HTTP/2 settings to client.\n\n        Should be called after the SSL handshake completes and\n        before processing any data.\n        \"\"\"\n        if self._initialized:\n            return\n\n        # Update local settings before initiating\n        self.h2_conn.update_settings({\n            _h2_settings.SettingCodes.MAX_CONCURRENT_STREAMS: self.max_concurrent_streams,\n            _h2_settings.SettingCodes.INITIAL_WINDOW_SIZE: self.initial_window_size,\n            _h2_settings.SettingCodes.MAX_FRAME_SIZE: self.max_frame_size,\n            _h2_settings.SettingCodes.MAX_HEADER_LIST_SIZE: self.max_header_list_size,\n        })\n\n        self.h2_conn.initiate_connection()\n        self._send_pending_data()\n        self._initialized = True\n\n    def receive_data(self, data=None):\n        \"\"\"Process received data and return completed requests.\n\n        Args:\n            data: Optional bytes to process. If None, reads from socket.\n\n        Returns:\n            list: List of HTTP2Request objects for completed requests\n\n        Raises:\n            HTTP2ConnectionError: On protocol or connection errors\n        \"\"\"\n        if data is None:\n            try:\n                data = self.sock.recv(self.READ_BUFFER_SIZE)\n            except (OSError, IOError) as e:\n                raise HTTP2ConnectionError(f\"Socket read error: {e}\")\n\n        if not data:\n            # Connection closed by peer\n            self._closed = True\n            return []\n\n        # Feed data to h2\n        # Note: Specific exceptions must come before ProtocolError (their parent class)\n        try:\n            events = self.h2_conn.receive_data(data)\n        except _h2_exceptions.FlowControlError as e:\n            # Send GOAWAY with FLOW_CONTROL_ERROR\n            self.close(error_code=HTTP2ErrorCode.FLOW_CONTROL_ERROR)\n            raise HTTP2ProtocolError(str(e))\n        except _h2_exceptions.FrameTooLargeError as e:\n            # Send GOAWAY with FRAME_SIZE_ERROR\n            self.close(error_code=HTTP2ErrorCode.FRAME_SIZE_ERROR)\n            raise HTTP2ProtocolError(str(e))\n        except _h2_exceptions.InvalidSettingsValueError as e:\n            # Use error_code from h2 exception (RFC 7540 Section 6.5.2):\n            # INITIAL_WINDOW_SIZE > 2^31-1 gives FLOW_CONTROL_ERROR\n            # Other invalid settings give PROTOCOL_ERROR\n            error_code = getattr(e, 'error_code', None)\n            if error_code is not None:\n                self.close(error_code=error_code)\n            else:\n                self.close(error_code=HTTP2ErrorCode.PROTOCOL_ERROR)\n            raise HTTP2ProtocolError(str(e))\n        except _h2_exceptions.TooManyStreamsError as e:\n            # Send GOAWAY with REFUSED_STREAM\n            self.close(error_code=HTTP2ErrorCode.REFUSED_STREAM)\n            raise HTTP2ProtocolError(str(e))\n        except _h2_exceptions.ProtocolError as e:\n            # Send GOAWAY with PROTOCOL_ERROR before raising\n            self.close(error_code=HTTP2ErrorCode.PROTOCOL_ERROR)\n            raise HTTP2ProtocolError(str(e))\n\n        # Process events\n        completed_requests = []\n        for event in events:\n            request = self._handle_event(event)\n            if request is not None:\n                completed_requests.append(request)\n\n        # Send any pending data (WINDOW_UPDATE, etc.)\n        self._send_pending_data()\n\n        return completed_requests\n\n    def _handle_event(self, event):\n        \"\"\"Handle a single h2 event.\n\n        Args:\n            event: h2 event object\n\n        Returns:\n            HTTP2Request if a request is complete, None otherwise\n        \"\"\"\n        if isinstance(event, _h2_events.RequestReceived):\n            return self._handle_request_received(event)\n\n        elif isinstance(event, _h2_events.DataReceived):\n            return self._handle_data_received(event)\n\n        elif isinstance(event, _h2_events.StreamEnded):\n            return self._handle_stream_ended(event)\n\n        elif isinstance(event, _h2_events.StreamReset):\n            self._handle_stream_reset(event)\n\n        elif isinstance(event, _h2_events.WindowUpdated):\n            pass  # Flow control update, handled by h2\n\n        elif isinstance(event, _h2_events.PriorityUpdated):\n            self._handle_priority_updated(event)\n\n        elif isinstance(event, _h2_events.SettingsAcknowledged):\n            pass  # Settings ACK received\n\n        elif isinstance(event, _h2_events.ConnectionTerminated):\n            self._handle_connection_terminated(event)\n\n        elif isinstance(event, _h2_events.TrailersReceived):\n            return self._handle_trailers_received(event)\n\n        return None\n\n    def _handle_request_received(self, event):\n        \"\"\"Handle RequestReceived event (HEADERS frame).\n\n        Args:\n            event: RequestReceived event with headers\n        \"\"\"\n        stream_id = event.stream_id\n        headers = event.headers\n\n        # Create new stream\n        stream = HTTP2Stream(stream_id, self)\n        self.streams[stream_id] = stream\n\n        # Process headers\n        # The StreamEnded event will come separately for GET/HEAD with no body\n        stream.receive_headers(headers, end_stream=False)\n\n    def _handle_data_received(self, event):\n        \"\"\"Handle DataReceived event.\n\n        Args:\n            event: DataReceived event with body data\n\n        Returns:\n            None (request completion handled by StreamEnded)\n        \"\"\"\n        stream_id = event.stream_id\n        data = event.data\n\n        stream = self.streams.get(stream_id)\n        if stream is None:\n            # Stream was reset or doesn't exist\n            return None\n\n        stream.receive_data(data, end_stream=False)\n\n        # Increment flow control windows (only if data received)\n        if len(data) > 0:\n            try:\n                # Update stream-level window\n                self.h2_conn.increment_flow_control_window(len(data), stream_id=stream_id)\n                # Update connection-level window\n                self.h2_conn.increment_flow_control_window(len(data), stream_id=None)\n                # Send WINDOW_UPDATE frames immediately\n                self._send_pending_data()\n            except (ValueError, _h2_exceptions.FlowControlError):\n                # Window overflow - send FLOW_CONTROL_ERROR and close\n                self.close(error_code=HTTP2ErrorCode.FLOW_CONTROL_ERROR)\n\n        return None\n\n    def _handle_stream_ended(self, event):\n        \"\"\"Handle StreamEnded event.\n\n        Args:\n            event: StreamEnded event\n\n        Returns:\n            HTTP2Request for the completed request\n        \"\"\"\n        stream_id = event.stream_id\n        stream = self.streams.get(stream_id)\n\n        if stream is None:\n            return None\n\n        # Mark stream as request complete\n        stream.request_complete = True\n\n        # Create request object\n        return HTTP2Request(stream, self.cfg, self.client_addr)\n\n    def _handle_stream_reset(self, event):\n        \"\"\"Handle StreamReset event (RST_STREAM frame).\n\n        Args:\n            event: StreamReset event\n        \"\"\"\n        stream_id = event.stream_id\n        stream = self.streams.get(stream_id)\n\n        if stream is not None:\n            stream.reset(event.error_code)\n            # Keep stream in dict for potential cleanup\n\n    def _handle_connection_terminated(self, event):\n        \"\"\"Handle ConnectionTerminated event (GOAWAY frame).\n\n        Args:\n            event: ConnectionTerminated event\n        \"\"\"\n        self._closed = True\n        # Could log event.error_code and event.additional_data\n\n    def _handle_trailers_received(self, event):\n        \"\"\"Handle TrailersReceived event.\n\n        Args:\n            event: TrailersReceived event with trailer headers\n\n        Returns:\n            HTTP2Request if this completes the request\n        \"\"\"\n        stream_id = event.stream_id\n        stream = self.streams.get(stream_id)\n\n        if stream is None:\n            return None\n\n        stream.receive_trailers(event.headers)\n\n        # Trailers always end the request\n        return HTTP2Request(stream, self.cfg, self.client_addr)\n\n    def _handle_priority_updated(self, event):\n        \"\"\"Handle PriorityUpdated event (PRIORITY frame).\n\n        Args:\n            event: PriorityUpdated event with priority info\n        \"\"\"\n        stream = self.streams.get(event.stream_id)\n        if stream is not None:\n            stream.update_priority(\n                weight=event.weight,\n                depends_on=event.depends_on,\n                exclusive=event.exclusive\n            )\n\n    def send_informational(self, stream_id, status, headers):\n        \"\"\"Send an informational response (1xx) on a stream.\n\n        This is used for 103 Early Hints and other 1xx responses.\n        Informational responses are sent before the final response\n        and do not end the stream.\n\n        Args:\n            stream_id: The stream ID\n            status: HTTP status code (100-199)\n            headers: List of (name, value) header tuples\n\n        Raises:\n            HTTP2Error: If status is not in 1xx range\n        \"\"\"\n        if status < 100 or status >= 200:\n            raise HTTP2Error(f\"Invalid informational status: {status}\")\n\n        stream = self.streams.get(stream_id)\n        if stream is None:\n            raise HTTP2Error(f\"Stream {stream_id} not found\")\n\n        # Build headers with :status pseudo-header\n        response_headers = [(':status', str(status))]\n        for name, value in headers:\n            # HTTP/2 headers must be lowercase\n            response_headers.append((name.lower(), str(value)))\n\n        # Send headers with end_stream=False (informational, more to follow)\n        self.h2_conn.send_headers(stream_id, response_headers, end_stream=False)\n        self._send_pending_data()\n\n    def send_response(self, stream_id, status, headers, body=None):\n        \"\"\"Send a response on a stream.\n\n        Args:\n            stream_id: The stream ID to respond on\n            status: HTTP status code (int)\n            headers: List of (name, value) header tuples\n            body: Optional response body bytes\n\n        Raises:\n            HTTP2Error: If stream not found or in invalid state\n\n        Returns:\n            bool: True if response sent, False if stream was already closed\n        \"\"\"\n        stream = self.streams.get(stream_id)\n        if stream is None:\n            # Stream was already cleaned up (reset/closed) - return gracefully\n            return False\n\n        # Build response headers with :status pseudo-header\n        response_headers = [(':status', str(status))]\n        for name, value in headers:\n            # HTTP/2 headers must be lowercase\n            response_headers.append((name.lower(), str(value)))\n\n        end_stream = body is None or len(body) == 0\n\n        try:\n            # Send headers\n            self.h2_conn.send_headers(stream_id, response_headers, end_stream=end_stream)\n            stream.send_headers(response_headers, end_stream=end_stream)\n            self._send_pending_data()\n\n            # Send body if present\n            if body and len(body) > 0:\n                self.send_data(stream_id, body, end_stream=True)\n            return True\n        except _h2_exceptions.StreamClosedError:\n            # Stream was reset by client - clean up gracefully\n            stream.close()\n            self.cleanup_stream(stream_id)\n            return False\n\n    def _wait_for_flow_control_window(self, stream_id):\n        \"\"\"Wait for flow control window to become positive.\n\n        Returns:\n            int: Available window size, or -1 if waiting failed\n        \"\"\"\n        import selectors\n\n        max_wait_attempts = 50  # ~5 seconds at 100ms per attempt\n        try:\n            sel = selectors.DefaultSelector()\n            sel.register(self.sock, selectors.EVENT_READ)\n        except (TypeError, ValueError):\n            # Socket doesn't support selectors (e.g., mock socket)\n            return -1\n\n        result = -1\n        try:\n            for _ in range(max_wait_attempts):\n                available = self.h2_conn.local_flow_control_window(stream_id)\n                if available > 0:\n                    result = available\n                    break\n\n                ready = sel.select(timeout=0.1)\n                if ready:\n                    try:\n                        incoming = self.sock.recv(self.READ_BUFFER_SIZE)\n                    except (OSError, IOError, _h2_exceptions.ProtocolError):\n                        break\n                    if not incoming:\n                        self._closed = True\n                        break\n                    try:\n                        events = self.h2_conn.receive_data(incoming)\n                    except _h2_exceptions.ProtocolError:\n                        break\n                    for event in events:\n                        if isinstance(event, _h2_events.StreamReset):\n                            if event.stream_id == stream_id:\n                                result = -1\n                                break\n                        elif isinstance(event, _h2_events.ConnectionTerminated):\n                            self._closed = True\n                            result = -1\n                            break\n                    else:\n                        self._send_pending_data()\n                        continue\n                    break  # Break outer loop if inner loop broke\n            else:\n                # Loop completed without break - check final window\n                result = self.h2_conn.local_flow_control_window(stream_id)\n        finally:\n            sel.close()\n\n        return result\n\n    def send_data(self, stream_id, data, end_stream=False):\n        \"\"\"Send data on a stream.\n\n        Args:\n            stream_id: The stream ID\n            data: Body data bytes\n            end_stream: Whether this ends the stream\n\n        Returns:\n            bool: True if data sent, False if stream was already closed\n        \"\"\"\n        stream = self.streams.get(stream_id)\n        if stream is None:\n            return False\n\n        data_to_send = data\n        try:\n            while data_to_send:\n                available = self.h2_conn.local_flow_control_window(stream_id)\n                chunk_size = min(available, self.max_frame_size, len(data_to_send))\n\n                if chunk_size <= 0:\n                    # Wait for WINDOW_UPDATE per RFC 7540 Section 6.9.2\n                    self._send_pending_data()\n                    available = self._wait_for_flow_control_window(stream_id)\n                    if available <= 0:\n                        return False\n                    chunk_size = min(available, self.max_frame_size, len(data_to_send))\n\n                chunk = data_to_send[:chunk_size]\n                data_to_send = data_to_send[chunk_size:]\n                is_final = end_stream and len(data_to_send) == 0\n\n                self.h2_conn.send_data(stream_id, chunk, end_stream=is_final)\n                self._send_pending_data()\n\n            stream.send_data(data, end_stream=end_stream)\n            return True\n        except (_h2_exceptions.StreamClosedError, _h2_exceptions.FlowControlError):\n            # Stream was reset by client or flow control error - clean up gracefully\n            stream.close()\n            self.cleanup_stream(stream_id)\n            return False\n\n    def send_trailers(self, stream_id, trailers):\n        \"\"\"Send trailing headers on a stream.\n\n        Trailers are headers sent after the response body, commonly used\n        for gRPC status codes, checksums, and timing information.\n\n        Args:\n            stream_id: The stream ID\n            trailers: List of (name, value) trailer tuples\n\n        Raises:\n            HTTP2Error: If stream not found, headers not sent, or pseudo-headers used\n\n        Returns:\n            bool: True if trailers sent, False if stream was already closed\n        \"\"\"\n        stream = self.streams.get(stream_id)\n        if stream is None:\n            # Stream was already cleaned up (reset/closed) - return gracefully\n            return False\n        if not stream.response_headers_sent:\n            # Can't send trailers without headers - return False\n            return False\n\n        # Validate and normalize trailer headers\n        trailer_headers = []\n        for name, value in trailers:\n            lname = name.lower()\n            if lname.startswith(':'):\n                raise HTTP2Error(f\"Pseudo-header '{name}' not allowed in trailers\")\n            trailer_headers.append((lname, str(value)))\n\n        try:\n            # Send trailers with end_stream=True\n            self.h2_conn.send_headers(stream_id, trailer_headers, end_stream=True)\n            stream.send_trailers(trailer_headers)\n            self._send_pending_data()\n            return True\n        except _h2_exceptions.StreamClosedError:\n            # Stream was reset by client - clean up gracefully\n            stream.close()\n            self.cleanup_stream(stream_id)\n            return False\n\n    def send_error(self, stream_id, status_code, message=None):\n        \"\"\"Send an error response on a stream.\n\n        Args:\n            stream_id: The stream ID\n            status_code: HTTP status code\n            message: Optional error message body\n        \"\"\"\n        body = message.encode() if message else b''\n        headers = [('content-length', str(len(body)))]\n        if body:\n            headers.append(('content-type', 'text/plain; charset=utf-8'))\n\n        self.send_response(stream_id, status_code, headers, body)\n\n    def reset_stream(self, stream_id, error_code=0x8):\n        \"\"\"Reset a stream with RST_STREAM.\n\n        Args:\n            stream_id: The stream ID to reset\n            error_code: HTTP/2 error code (default: CANCEL)\n        \"\"\"\n        stream = self.streams.get(stream_id)\n        if stream is not None:\n            stream.reset(error_code)\n\n        self.h2_conn.reset_stream(stream_id, error_code=error_code)\n        self._send_pending_data()\n\n    def close(self, error_code=0x0, last_stream_id=None):\n        \"\"\"Close the connection gracefully with GOAWAY.\n\n        Args:\n            error_code: HTTP/2 error code (default: NO_ERROR)\n            last_stream_id: Last processed stream ID (default: highest)\n        \"\"\"\n        if self._closed:\n            return\n\n        self._closed = True\n\n        if last_stream_id is None:\n            # Use highest stream ID we've seen\n            last_stream_id = max(self.streams.keys()) if self.streams else 0\n\n        try:\n            self.h2_conn.close_connection(error_code=error_code)\n            self._send_pending_data()\n        except Exception:\n            pass  # Best effort\n\n    def _send_pending_data(self):\n        \"\"\"Send any pending data from h2 to the socket.\"\"\"\n        data = self.h2_conn.data_to_send()\n        if data:\n            try:\n                self.sock.sendall(data)\n            except (OSError, IOError) as e:\n                self._closed = True\n                raise HTTP2ConnectionError(f\"Socket write error: {e}\")\n\n    @property\n    def is_closed(self):\n        \"\"\"Check if connection is closed.\"\"\"\n        return self._closed\n\n    def cleanup_stream(self, stream_id):\n        \"\"\"Remove a stream after processing is complete.\n\n        Args:\n            stream_id: The stream ID to clean up\n        \"\"\"\n        self.streams.pop(stream_id, None)\n\n    def __repr__(self):\n        return (\n            f\"<HTTP2ServerConnection \"\n            f\"streams={len(self.streams)} \"\n            f\"closed={self._closed}>\"\n        )\n\n\n__all__ = ['HTTP2ServerConnection']\n"
  },
  {
    "path": "gunicorn/http2/errors.py",
    "content": "# -*- coding: utf-8 -\n#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nHTTP/2 specific exceptions.\n\nThese exceptions map to HTTP/2 error codes defined in RFC 7540.\n\"\"\"\n\n\nclass HTTP2ErrorCode:\n    \"\"\"HTTP/2 Error Codes (RFC 7540 Section 7).\"\"\"\n\n    NO_ERROR = 0x0\n    PROTOCOL_ERROR = 0x1\n    INTERNAL_ERROR = 0x2\n    FLOW_CONTROL_ERROR = 0x3\n    SETTINGS_TIMEOUT = 0x4\n    STREAM_CLOSED = 0x5\n    FRAME_SIZE_ERROR = 0x6\n    REFUSED_STREAM = 0x7\n    CANCEL = 0x8\n    COMPRESSION_ERROR = 0x9\n    CONNECT_ERROR = 0xa\n    ENHANCE_YOUR_CALM = 0xb\n    INADEQUATE_SECURITY = 0xc\n    HTTP_1_1_REQUIRED = 0xd\n\n\nclass HTTP2Error(Exception):\n    \"\"\"Base exception for HTTP/2 errors.\"\"\"\n\n    error_code = 0x0  # NO_ERROR\n\n    def __init__(self, message=None, error_code=None):\n        self.message = message or self.__class__.__doc__\n        if error_code is not None:\n            self.error_code = error_code\n        super().__init__(self.message)\n\n\nclass HTTP2ProtocolError(HTTP2Error):\n    \"\"\"Protocol error detected.\"\"\"\n\n    error_code = 0x1  # PROTOCOL_ERROR\n\n\nclass HTTP2InternalError(HTTP2Error):\n    \"\"\"Internal error occurred.\"\"\"\n\n    error_code = 0x2  # INTERNAL_ERROR\n\n\nclass HTTP2FlowControlError(HTTP2Error):\n    \"\"\"Flow control limits exceeded.\"\"\"\n\n    error_code = 0x3  # FLOW_CONTROL_ERROR\n\n\nclass HTTP2SettingsTimeout(HTTP2Error):\n    \"\"\"Settings acknowledgment timeout.\"\"\"\n\n    error_code = 0x4  # SETTINGS_TIMEOUT\n\n\nclass HTTP2StreamClosed(HTTP2Error):\n    \"\"\"Stream was closed.\"\"\"\n\n    error_code = 0x5  # STREAM_CLOSED\n\n\nclass HTTP2FrameSizeError(HTTP2Error):\n    \"\"\"Frame size is incorrect.\"\"\"\n\n    error_code = 0x6  # FRAME_SIZE_ERROR\n\n\nclass HTTP2RefusedStream(HTTP2Error):\n    \"\"\"Stream was refused.\"\"\"\n\n    error_code = 0x7  # REFUSED_STREAM\n\n\nclass HTTP2Cancel(HTTP2Error):\n    \"\"\"Stream was cancelled.\"\"\"\n\n    error_code = 0x8  # CANCEL\n\n\nclass HTTP2CompressionError(HTTP2Error):\n    \"\"\"Compression state error.\"\"\"\n\n    error_code = 0x9  # COMPRESSION_ERROR\n\n\nclass HTTP2ConnectError(HTTP2Error):\n    \"\"\"Connection error during CONNECT.\"\"\"\n\n    error_code = 0xa  # CONNECT_ERROR\n\n\nclass HTTP2EnhanceYourCalm(HTTP2Error):\n    \"\"\"Peer is generating excessive load.\"\"\"\n\n    error_code = 0xb  # ENHANCE_YOUR_CALM\n\n\nclass HTTP2InadequateSecurity(HTTP2Error):\n    \"\"\"Transport security is inadequate.\"\"\"\n\n    error_code = 0xc  # INADEQUATE_SECURITY\n\n\nclass HTTP2RequiresHTTP11(HTTP2Error):\n    \"\"\"HTTP/1.1 is required for this request.\"\"\"\n\n    error_code = 0xd  # HTTP_1_1_REQUIRED\n\n\nclass HTTP2StreamError(HTTP2Error):\n    \"\"\"Error specific to a single stream.\"\"\"\n\n    def __init__(self, stream_id, message=None, error_code=None):\n        self.stream_id = stream_id\n        super().__init__(message, error_code)\n\n    def __str__(self):\n        return f\"Stream {self.stream_id}: {self.message}\"\n\n\nclass HTTP2ConnectionError(HTTP2Error):\n    \"\"\"Error affecting the entire connection.\"\"\"\n\n\nclass HTTP2ConfigurationError(HTTP2Error):\n    \"\"\"Invalid HTTP/2 configuration.\"\"\"\n\n\nclass HTTP2NotAvailable(HTTP2Error):\n    \"\"\"HTTP/2 support is not available (h2 library not installed).\"\"\"\n\n    def __init__(self, message=None):\n        message = message or \"HTTP/2 requires the h2 library: pip install gunicorn[http2]\"\n        super().__init__(message)\n\n\n__all__ = [\n    'HTTP2ErrorCode',\n    'HTTP2Error',\n    'HTTP2ProtocolError',\n    'HTTP2InternalError',\n    'HTTP2FlowControlError',\n    'HTTP2SettingsTimeout',\n    'HTTP2StreamClosed',\n    'HTTP2FrameSizeError',\n    'HTTP2RefusedStream',\n    'HTTP2Cancel',\n    'HTTP2CompressionError',\n    'HTTP2ConnectError',\n    'HTTP2EnhanceYourCalm',\n    'HTTP2InadequateSecurity',\n    'HTTP2RequiresHTTP11',\n    'HTTP2StreamError',\n    'HTTP2ConnectionError',\n    'HTTP2ConfigurationError',\n    'HTTP2NotAvailable',\n]\n"
  },
  {
    "path": "gunicorn/http2/request.py",
    "content": "# -*- coding: utf-8 -\n#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nHTTP/2 request wrapper.\n\nProvides a Request-compatible interface for HTTP/2 streams.\n\"\"\"\n\nfrom io import BytesIO\n\nfrom gunicorn.util import split_request_uri\n\n\nclass HTTP2Body:\n    \"\"\"Body wrapper for HTTP/2 request data.\n\n    Provides a file-like interface to the request body,\n    compatible with gunicorn's Body class expectations.\n    \"\"\"\n\n    def __init__(self, data):\n        \"\"\"Initialize with body data.\n\n        Args:\n            data: bytes containing the request body\n        \"\"\"\n        self._data = BytesIO(data)\n        self._len = len(data)\n\n    def read(self, size=None):\n        \"\"\"Read data from the body.\n\n        Args:\n            size: Number of bytes to read, or None for all remaining\n\n        Returns:\n            bytes: The requested data\n        \"\"\"\n        if size is None:\n            return self._data.read()\n        return self._data.read(size)\n\n    def readline(self, size=None):\n        \"\"\"Read a line from the body.\n\n        Args:\n            size: Maximum bytes to read\n\n        Returns:\n            bytes: A line of data\n        \"\"\"\n        if size is None:\n            return self._data.readline()\n        return self._data.readline(size)\n\n    def readlines(self, hint=None):\n        \"\"\"Read all lines from the body.\n\n        Args:\n            hint: Approximate byte count hint\n\n        Returns:\n            list: List of lines\n        \"\"\"\n        return self._data.readlines(hint)\n\n    def __iter__(self):\n        \"\"\"Iterate over lines in the body.\"\"\"\n        return iter(self._data)\n\n    def __len__(self):\n        \"\"\"Return the content length.\"\"\"\n        return self._len\n\n    def close(self):\n        \"\"\"Close the body stream.\"\"\"\n        self._data.close()\n\n\nclass HTTP2Request:\n    \"\"\"HTTP/2 request wrapper compatible with gunicorn Request interface.\n\n    Wraps an HTTP2Stream to provide the same interface as the HTTP/1.x\n    Request class, allowing workers to handle HTTP/2 requests using\n    existing code paths.\n    \"\"\"\n\n    def __init__(self, stream, cfg, peer_addr):\n        \"\"\"Initialize from an HTTP/2 stream.\n\n        Args:\n            stream: HTTP2Stream instance with received headers/body\n            cfg: Gunicorn configuration object\n            peer_addr: Client address tuple (host, port)\n        \"\"\"\n        self.stream = stream\n        self.cfg = cfg\n        self.peer_addr = peer_addr\n        self.remote_addr = peer_addr\n\n        # HTTP/2 version tuple\n        self.version = (2, 0)\n\n        # Parse pseudo-headers\n        pseudo = stream.get_pseudo_headers()\n        self.method = pseudo.get(':method', 'GET')\n        self.scheme = pseudo.get(':scheme', 'https')\n        authority = pseudo.get(':authority', '')\n        path = pseudo.get(':path', '/')\n\n        # Parse the path into components\n        self.uri = path\n        try:\n            parts = split_request_uri(path)\n            self.path = parts.path or \"\"\n            self.query = parts.query or \"\"\n            self.fragment = parts.fragment or \"\"\n        except ValueError:\n            self.path = path\n            self.query = \"\"\n            self.fragment = \"\"\n\n        # Store authority for Host header equivalent\n        self._authority = authority\n\n        # Convert HTTP/2 headers to HTTP/1.1 style\n        # HTTP/2 headers are lowercase, convert to uppercase for WSGI\n        self.headers = []\n        for name, value in stream.get_regular_headers():\n            # Convert to uppercase for WSGI compatibility\n            self.headers.append((name.upper(), value))\n\n        # Set Host header from :authority (RFC 9113 section 8.3.1)\n        # :authority MUST take precedence over Host header\n        if authority:\n            self.headers = [(n, v) for n, v in self.headers if n != 'HOST']\n            self.headers.append(('HOST', authority))\n\n        # Trailers (if any)\n        self.trailers = []\n        if stream.trailers:\n            self.trailers = [\n                (name.upper(), value)\n                for name, value in stream.trailers\n            ]\n\n        # Body - HTTP/2 streams have complete body data\n        body_data = stream.get_request_body()\n        self.body = HTTP2Body(body_data)\n\n        # Connection state\n        self.must_close = False\n        self._expected_100_continue = False\n\n        # Request numbering (for logging)\n        self.req_number = stream.stream_id\n\n        # HTTP/2 does not use proxy protocol through the data stream\n        self.proxy_protocol_info = None\n\n        # Stream priority (RFC 7540 Section 5.3)\n        self.priority_weight = stream.priority_weight\n        self.priority_depends_on = stream.priority_depends_on\n\n    def force_close(self):\n        \"\"\"Force the connection to close after this request.\"\"\"\n        self.must_close = True\n\n    def should_close(self):\n        \"\"\"Check if connection should close after this request.\n\n        HTTP/2 connections are persistent by design, but we may still\n        need to close if explicitly requested.\n\n        Returns:\n            bool: True if connection should close\n        \"\"\"\n        if self.must_close:\n            return True\n        # HTTP/2 connections are persistent, don't close by default\n        return False\n\n    def get_header(self, name):\n        \"\"\"Get a header value by name.\n\n        Args:\n            name: Header name (case-insensitive)\n\n        Returns:\n            str: Header value, or None if not found\n        \"\"\"\n        name = name.upper()\n        for h_name, h_value in self.headers:\n            if h_name == name:\n                return h_value\n        return None\n\n    @property\n    def content_length(self):\n        \"\"\"Get the Content-Length header value.\n\n        Returns:\n            int: Content length, or None if not set\n        \"\"\"\n        cl = self.get_header('CONTENT-LENGTH')\n        if cl is not None:\n            try:\n                return int(cl)\n            except ValueError:\n                pass\n        return None\n\n    @property\n    def content_type(self):\n        \"\"\"Get the Content-Type header value.\n\n        Returns:\n            str: Content type, or None if not set\n        \"\"\"\n        return self.get_header('CONTENT-TYPE')\n\n    def __repr__(self):\n        return (\n            f\"<HTTP2Request \"\n            f\"method={self.method} \"\n            f\"path={self.path} \"\n            f\"stream_id={self.stream.stream_id}>\"\n        )\n\n\n__all__ = ['HTTP2Request', 'HTTP2Body']\n"
  },
  {
    "path": "gunicorn/http2/stream.py",
    "content": "# -*- coding: utf-8 -\n#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nHTTP/2 stream state management.\n\nEach HTTP/2 stream represents a single request/response exchange.\n\"\"\"\n\nfrom enum import Enum, auto\nfrom io import BytesIO\n\nfrom .errors import HTTP2StreamError\n\n\nclass StreamState(Enum):\n    \"\"\"HTTP/2 stream states as defined in RFC 7540 Section 5.1.\"\"\"\n\n    IDLE = auto()\n    RESERVED_LOCAL = auto()\n    RESERVED_REMOTE = auto()\n    OPEN = auto()\n    HALF_CLOSED_LOCAL = auto()\n    HALF_CLOSED_REMOTE = auto()\n    CLOSED = auto()\n\n\nclass HTTP2Stream:\n    \"\"\"Represents a single HTTP/2 stream.\n\n    Manages stream state, headers, and body data for a single\n    request/response exchange within an HTTP/2 connection.\n    \"\"\"\n\n    def __init__(self, stream_id, connection):\n        \"\"\"Initialize an HTTP/2 stream.\n\n        Args:\n            stream_id: The unique stream identifier (odd for client-initiated)\n            connection: The parent HTTP2ServerConnection\n        \"\"\"\n        self.stream_id = stream_id\n        self.connection = connection\n\n        # Stream state\n        self.state = StreamState.IDLE\n\n        # Request data\n        self.request_headers = []\n        self.request_body = BytesIO()\n        self.request_complete = False\n\n        # Response data\n        self.response_started = False\n        self.response_headers_sent = False\n        self.response_complete = False\n\n        # Flow control\n        self.window_size = connection.initial_window_size\n\n        # Request trailers\n        self.trailers = None\n\n        # Response trailers\n        self.response_trailers = None\n\n        # Stream priority (RFC 7540 Section 5.3)\n        self.priority_weight = 16\n        self.priority_depends_on = 0\n        self.priority_exclusive = False\n\n    @property\n    def is_client_stream(self):\n        \"\"\"Check if this is a client-initiated stream (odd stream ID).\"\"\"\n        return self.stream_id % 2 == 1\n\n    @property\n    def is_server_stream(self):\n        \"\"\"Check if this is a server-initiated stream (even stream ID).\"\"\"\n        return self.stream_id % 2 == 0\n\n    @property\n    def can_receive(self):\n        \"\"\"Check if this stream can receive data.\"\"\"\n        return self.state in (\n            StreamState.OPEN,\n            StreamState.HALF_CLOSED_LOCAL,\n        )\n\n    @property\n    def can_send(self):\n        \"\"\"Check if this stream can send data.\"\"\"\n        return self.state in (\n            StreamState.OPEN,\n            StreamState.HALF_CLOSED_REMOTE,\n        )\n\n    def receive_headers(self, headers, end_stream=False):\n        \"\"\"Process received HEADERS frame.\n\n        Args:\n            headers: List of (name, value) tuples\n            end_stream: True if END_STREAM flag is set\n\n        Raises:\n            HTTP2StreamError: If headers received in invalid state\n        \"\"\"\n        if self.state == StreamState.IDLE:\n            self.state = StreamState.OPEN\n        elif self.state not in (StreamState.OPEN, StreamState.HALF_CLOSED_LOCAL):\n            raise HTTP2StreamError(\n                self.stream_id,\n                f\"Cannot receive headers in state {self.state.name}\"\n            )\n\n        self.request_headers.extend(headers)\n\n        if end_stream:\n            self._half_close_remote()\n            self.request_complete = True\n\n    def receive_data(self, data, end_stream=False):\n        \"\"\"Process received DATA frame.\n\n        Args:\n            data: Bytes received\n            end_stream: True if END_STREAM flag is set\n\n        Raises:\n            HTTP2StreamError: If data received in invalid state\n        \"\"\"\n        if not self.can_receive:\n            raise HTTP2StreamError(\n                self.stream_id,\n                f\"Cannot receive data in state {self.state.name}\"\n            )\n\n        self.request_body.write(data)\n\n        if end_stream:\n            self._half_close_remote()\n            self.request_complete = True\n\n    def receive_trailers(self, trailers):\n        \"\"\"Process received trailing headers.\n\n        Args:\n            trailers: List of (name, value) tuples\n        \"\"\"\n        if not self.can_receive:\n            raise HTTP2StreamError(\n                self.stream_id,\n                f\"Cannot receive trailers in state {self.state.name}\"\n            )\n\n        self.trailers = trailers\n        self._half_close_remote()\n        self.request_complete = True\n\n    def send_headers(self, headers, end_stream=False):\n        \"\"\"Mark headers as sent.\n\n        Args:\n            headers: List of (name, value) tuples to send\n            end_stream: True if this completes the response\n\n        Raises:\n            HTTP2StreamError: If headers cannot be sent in current state\n        \"\"\"\n        if not self.can_send:\n            raise HTTP2StreamError(\n                self.stream_id,\n                f\"Cannot send headers in state {self.state.name}\"\n            )\n\n        self.response_started = True\n        self.response_headers_sent = True\n\n        if end_stream:\n            self._half_close_local()\n            self.response_complete = True\n\n    def send_data(self, data, end_stream=False):\n        \"\"\"Mark data as sent.\n\n        Args:\n            data: Bytes to send\n            end_stream: True if this completes the response\n\n        Raises:\n            HTTP2StreamError: If data cannot be sent in current state\n        \"\"\"\n        if not self.can_send:\n            raise HTTP2StreamError(\n                self.stream_id,\n                f\"Cannot send data in state {self.state.name}\"\n            )\n\n        if end_stream:\n            self._half_close_local()\n            self.response_complete = True\n\n    def send_trailers(self, trailers):\n        \"\"\"Mark trailers as sent and close the stream.\n\n        Args:\n            trailers: List of (name, value) trailer tuples\n\n        Raises:\n            HTTP2StreamError: If trailers cannot be sent in current state\n        \"\"\"\n        if not self.can_send:\n            raise HTTP2StreamError(\n                self.stream_id,\n                f\"Cannot send trailers in state {self.state.name}\"\n            )\n        self.response_trailers = trailers\n        self._half_close_local()\n        self.response_complete = True\n\n    def reset(self, error_code=0x8):\n        \"\"\"Reset this stream with RST_STREAM.\n\n        Args:\n            error_code: HTTP/2 error code (default: CANCEL)\n        \"\"\"\n        self.state = StreamState.CLOSED\n        self.response_complete = True\n        self.request_complete = True\n\n    def close(self):\n        \"\"\"Close this stream normally.\"\"\"\n        self.state = StreamState.CLOSED\n        self.response_complete = True\n        self.request_complete = True\n\n    def update_priority(self, weight=None, depends_on=None, exclusive=None):\n        \"\"\"Update stream priority from PRIORITY frame.\n\n        Args:\n            weight: Priority weight (1-256), higher = more resources\n            depends_on: Stream ID this stream depends on\n            exclusive: Whether this is an exclusive dependency\n        \"\"\"\n        if weight is not None:\n            self.priority_weight = max(1, min(256, weight))\n        if depends_on is not None:\n            self.priority_depends_on = depends_on\n        if exclusive is not None:\n            self.priority_exclusive = exclusive\n\n    def _half_close_local(self):\n        \"\"\"Transition to half-closed (local) state.\"\"\"\n        if self.state == StreamState.OPEN:\n            self.state = StreamState.HALF_CLOSED_LOCAL\n        elif self.state == StreamState.HALF_CLOSED_REMOTE:\n            self.state = StreamState.CLOSED\n        else:\n            raise HTTP2StreamError(\n                self.stream_id,\n                f\"Cannot half-close local in state {self.state.name}\"\n            )\n\n    def _half_close_remote(self):\n        \"\"\"Transition to half-closed (remote) state.\"\"\"\n        if self.state == StreamState.OPEN:\n            self.state = StreamState.HALF_CLOSED_REMOTE\n        elif self.state == StreamState.HALF_CLOSED_LOCAL:\n            self.state = StreamState.CLOSED\n        else:\n            raise HTTP2StreamError(\n                self.stream_id,\n                f\"Cannot half-close remote in state {self.state.name}\"\n            )\n\n    def get_request_body(self):\n        \"\"\"Get the complete request body.\n\n        Returns:\n            bytes: The request body data\n        \"\"\"\n        return self.request_body.getvalue()\n\n    def get_pseudo_headers(self):\n        \"\"\"Extract HTTP/2 pseudo-headers from request headers.\n\n        Returns:\n            dict: Mapping of pseudo-header names to values\n                  (e.g., {':method': 'GET', ':path': '/'})\n        \"\"\"\n        pseudo = {}\n        for name, value in self.request_headers:\n            if name.startswith(':'):\n                pseudo[name] = value\n        return pseudo\n\n    def get_regular_headers(self):\n        \"\"\"Get regular (non-pseudo) headers from request.\n\n        Returns:\n            list: List of (name, value) tuples for regular headers\n        \"\"\"\n        return [\n            (name, value)\n            for name, value in self.request_headers\n            if not name.startswith(':')\n        ]\n\n    def __repr__(self):\n        return (\n            f\"<HTTP2Stream id={self.stream_id} \"\n            f\"state={self.state.name} \"\n            f\"req_complete={self.request_complete} \"\n            f\"resp_complete={self.response_complete}>\"\n        )\n\n\n__all__ = ['HTTP2Stream', 'StreamState']\n"
  },
  {
    "path": "gunicorn/instrument/__init__.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n"
  },
  {
    "path": "gunicorn/instrument/statsd.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"Bare-bones implementation of statsD's protocol, client-side\"\n\nimport logging\nimport socket\nfrom re import sub\n\nfrom gunicorn.glogging import Logger\n\n# Instrumentation constants\nMETRIC_VAR = \"metric\"\nVALUE_VAR = \"value\"\nMTYPE_VAR = \"mtype\"\nGAUGE_TYPE = \"gauge\"\nCOUNTER_TYPE = \"counter\"\nHISTOGRAM_TYPE = \"histogram\"\nTIMER_TYPE = \"timer\"\n\n\nclass Statsd(Logger):\n    \"\"\"statsD-based instrumentation, that passes as a logger\n    \"\"\"\n    def __init__(self, cfg):\n        Logger.__init__(self, cfg)\n        self.prefix = sub(r\"^(.+[^.]+)\\.*$\", \"\\\\g<1>.\", cfg.statsd_prefix)\n\n        if isinstance(cfg.statsd_host, str):\n            address_family = socket.AF_UNIX\n        else:\n            address_family = socket.AF_INET\n\n        try:\n            self.sock = socket.socket(address_family, socket.SOCK_DGRAM)\n            self.sock.connect(cfg.statsd_host)\n        except Exception:\n            self.sock = None\n\n        self.dogstatsd_tags = cfg.dogstatsd_tags\n\n    # Log errors and warnings\n    def critical(self, msg, *args, **kwargs):\n        Logger.critical(self, msg, *args, **kwargs)\n        self.increment(\"gunicorn.log.critical\", 1)\n\n    def error(self, msg, *args, **kwargs):\n        Logger.error(self, msg, *args, **kwargs)\n        self.increment(\"gunicorn.log.error\", 1)\n\n    def warning(self, msg, *args, **kwargs):\n        Logger.warning(self, msg, *args, **kwargs)\n        self.increment(\"gunicorn.log.warning\", 1)\n\n    def exception(self, msg, *args, **kwargs):\n        Logger.exception(self, msg, *args, **kwargs)\n        self.increment(\"gunicorn.log.exception\", 1)\n\n    # Special treatment for info, the most common log level\n    def info(self, msg, *args, **kwargs):\n        self.log(logging.INFO, msg, *args, **kwargs)\n\n    # skip the run-of-the-mill logs\n    def debug(self, msg, *args, **kwargs):\n        self.log(logging.DEBUG, msg, *args, **kwargs)\n\n    def log(self, lvl, msg, *args, **kwargs):\n        \"\"\"Log a given statistic if metric, value and type are present\n        \"\"\"\n        try:\n            extra = kwargs.get(\"extra\", None)\n            if extra is not None:\n                metric = extra.get(METRIC_VAR, None)\n                value = extra.get(VALUE_VAR, None)\n                typ = extra.get(MTYPE_VAR, None)\n                if metric and value and typ:\n                    if typ == GAUGE_TYPE:\n                        self.gauge(metric, value)\n                    elif typ == COUNTER_TYPE:\n                        self.increment(metric, value)\n                    elif typ == HISTOGRAM_TYPE:\n                        self.histogram(metric, value)\n                    elif typ == TIMER_TYPE:\n                        self.timer(metric, value)\n                    else:\n                        pass\n\n            # Log to parent logger only if there is something to say\n            if msg:\n                Logger.log(self, lvl, msg, *args, **kwargs)\n        except Exception:\n            Logger.warning(self, \"Failed to log to statsd\", exc_info=True)\n\n    # access logging\n    def access(self, resp, req, environ, request_time):\n        \"\"\"Measure request duration\n        request_time is a datetime.timedelta\n        \"\"\"\n        Logger.access(self, resp, req, environ, request_time)\n        duration_in_ms = request_time.seconds * 1000 + float(request_time.microseconds) / 10 ** 3\n        status = resp.status\n        if isinstance(status, bytes):\n            status = status.decode('utf-8')\n        if isinstance(status, str):\n            status = int(status.split(None, 1)[0])\n        self.timer(\"gunicorn.request.duration\", duration_in_ms)\n        self.increment(\"gunicorn.requests\", 1)\n        self.increment(\"gunicorn.request.status.%d\" % status, 1)\n\n    # statsD methods\n    # you can use those directly if you want\n    def gauge(self, name, value):\n        self._sock_send(\"{0}{1}:{2}|g\".format(self.prefix, name, value))\n\n    def increment(self, name, value, sampling_rate=1.0):\n        self._sock_send(\"{0}{1}:{2}|c|@{3}\".format(self.prefix, name, value, sampling_rate))\n\n    def decrement(self, name, value, sampling_rate=1.0):\n        self._sock_send(\"{0}{1}:-{2}|c|@{3}\".format(self.prefix, name, value, sampling_rate))\n\n    def timer(self, name, value):\n        self._sock_send(\"{0}{1}:{2}|ms\".format(self.prefix, name, value))\n\n    def histogram(self, name, value):\n        self._sock_send(\"{0}{1}:{2}|h\".format(self.prefix, name, value))\n\n    def _sock_send(self, msg):\n        try:\n            if isinstance(msg, str):\n                msg = msg.encode(\"ascii\")\n\n            # http://docs.datadoghq.com/guides/dogstatsd/#datagram-format\n            if self.dogstatsd_tags:\n                msg = msg + b\"|#\" + self.dogstatsd_tags.encode('ascii')\n\n            if self.sock:\n                self.sock.send(msg)\n        except Exception:\n            Logger.warning(self, \"Error sending message to statsd\", exc_info=True)\n"
  },
  {
    "path": "gunicorn/pidfile.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport errno\nimport os\nimport tempfile\n\n\nclass Pidfile:\n    \"\"\"\\\n    Manage a PID file. If a specific name is provided\n    it and '\"%s.oldpid\" % name' will be used. Otherwise\n    we create a temp file using os.mkstemp.\n    \"\"\"\n\n    def __init__(self, fname):\n        self.fname = fname\n        self.pid = None\n\n    def create(self, pid):\n        oldpid = self.validate()\n        if oldpid:\n            if oldpid == os.getpid():\n                return\n            msg = \"Already running on PID %s (or pid file '%s' is stale)\"\n            raise RuntimeError(msg % (oldpid, self.fname))\n\n        self.pid = pid\n\n        # Write pidfile\n        fdir = os.path.dirname(self.fname)\n        if fdir and not os.path.isdir(fdir):\n            raise RuntimeError(\"%s doesn't exist. Can't create pidfile.\" % fdir)\n        fd, fname = tempfile.mkstemp(dir=fdir)\n        try:\n            os.write(fd, (\"%s\\n\" % self.pid).encode('utf-8'))\n            if self.fname:\n                os.rename(fname, self.fname)\n            else:\n                self.fname = fname\n        finally:\n            os.close(fd)\n\n        # set permissions to -rw-r--r--\n        os.chmod(self.fname, 420)\n\n    def rename(self, path):\n        self.unlink()\n        self.fname = path\n        self.create(self.pid)\n\n    def unlink(self):\n        \"\"\" delete pidfile\"\"\"\n        try:\n            with open(self.fname) as f:\n                pid1 = int(f.read() or 0)\n\n            if pid1 == self.pid:\n                os.unlink(self.fname)\n        except Exception:\n            pass\n\n    def validate(self):\n        \"\"\" Validate pidfile and make it stale if needed\"\"\"\n        if not self.fname:\n            return\n        try:\n            with open(self.fname) as f:\n                try:\n                    wpid = int(f.read())\n                except ValueError:\n                    return\n\n                try:\n                    os.kill(wpid, 0)\n                    return wpid\n                except OSError as e:\n                    if e.args[0] == errno.EPERM:\n                        return wpid\n                    if e.args[0] == errno.ESRCH:\n                        return\n                    raise\n        except OSError as e:\n            if e.args[0] == errno.ENOENT:\n                return\n            raise\n"
  },
  {
    "path": "gunicorn/reloader.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n# pylint: disable=no-else-continue\n\nimport os\nimport os.path\nimport re\nimport sys\nimport time\nimport threading\n\nCOMPILED_EXT_RE = re.compile(r'py[co]$')\n\n\nclass ReloaderBase(threading.Thread):\n    def __init__(self, extra_files=None, interval=1, callback=None):\n        super().__init__()\n        self.daemon = True\n        self._extra_files = set(extra_files or ())\n        self._interval = interval\n        self._callback = callback\n\n    def add_extra_file(self, filename):\n        self._extra_files.add(filename)\n\n    def get_files(self):\n        fnames = [\n            COMPILED_EXT_RE.sub('py', module.__file__)\n            for module in tuple(sys.modules.values())\n            if getattr(module, '__file__', None)\n        ]\n\n        fnames.extend(self._extra_files)\n\n        return fnames\n\n\nclass Reloader(ReloaderBase):\n    def run(self):\n        mtimes = {}\n        while True:\n            for filename in self.get_files():\n                try:\n                    mtime = os.stat(filename).st_mtime\n                except OSError:\n                    continue\n                old_time = mtimes.get(filename)\n                if old_time is None:\n                    mtimes[filename] = mtime\n                    continue\n                elif mtime > old_time:\n                    if self._callback:\n                        self._callback(filename)\n            time.sleep(self._interval)\n\n\nhas_inotify = False\nif sys.platform.startswith('linux'):\n    try:\n        from inotify.adapters import Inotify\n        import inotify.constants\n        has_inotify = True\n    except ImportError:\n        pass\n\n\nif has_inotify:\n\n    class InotifyReloader(ReloaderBase):\n        event_mask = (inotify.constants.IN_CREATE | inotify.constants.IN_DELETE\n                      | inotify.constants.IN_DELETE_SELF | inotify.constants.IN_MODIFY\n                      | inotify.constants.IN_MOVE_SELF | inotify.constants.IN_MOVED_FROM\n                      | inotify.constants.IN_MOVED_TO)\n\n        def __init__(self, extra_files=None, callback=None):\n            super().__init__(extra_files=extra_files, callback=callback)\n            self._dirs = set()\n            self._watcher = Inotify()\n\n        def add_extra_file(self, filename):\n            super().add_extra_file(filename)\n\n            dirname = os.path.dirname(filename)\n            if dirname in self._dirs:\n                return\n\n            self._watcher.add_watch(dirname, mask=self.event_mask)\n            self._dirs.add(dirname)\n\n        def get_dirs(self):\n            dirnames = [os.path.dirname(os.path.abspath(fname)) for fname in self.get_files()]\n            return set(dirnames)\n\n        def refresh_dirs(self):\n            new_dirs = self.get_dirs().difference(self._dirs)\n            self._dirs.update(new_dirs)\n            for new_dir in new_dirs:\n                if os.path.isdir(new_dir):\n                    self._watcher.add_watch(new_dir, mask=self.event_mask)\n\n        def run(self):\n            self.refresh_dirs()\n\n            for event in self._watcher.event_gen():\n                if event is None:\n                    self.refresh_dirs()\n                    continue\n\n                filename = event[3]\n\n                self._callback(filename)\n\nelse:\n\n    class InotifyReloader:\n        def __init__(self, extra_files=None, callback=None):\n            raise ImportError('You must have the inotify module installed to '\n                              'use the inotify reloader')\n\n\npreferred_reloader = InotifyReloader if has_inotify else Reloader\n\nreloader_engines = {\n    'auto': preferred_reloader,\n    'poll': Reloader,\n    'inotify': InotifyReloader,\n}\n"
  },
  {
    "path": "gunicorn/sock.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport errno\nimport os\nimport socket\nimport ssl\nimport stat\nimport struct\nimport sys\nimport time\n\nfrom gunicorn import util\n\nPLATFORM = sys.platform\n\n\nclass BaseSocket:\n\n    def __init__(self, address, conf, log, fd=None):\n        self.log = log\n        self.conf = conf\n\n        self.cfg_addr = address\n        if fd is None:\n            sock = socket.socket(self.FAMILY, socket.SOCK_STREAM)\n            bound = False\n        else:\n            sock = socket.fromfd(fd, self.FAMILY, socket.SOCK_STREAM)\n            os.close(fd)\n            bound = True\n\n        self.sock = self.set_options(sock, bound=bound)\n\n    def __str__(self):\n        return \"<socket %d>\" % self.sock.fileno()\n\n    def __getattr__(self, name):\n        return getattr(self.sock, name)\n\n    def set_options(self, sock, bound=False):\n        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n        if (self.conf.reuse_port\n                and hasattr(socket, 'SO_REUSEPORT')):  # pragma: no cover\n            try:\n                sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)\n            except OSError as err:\n                if err.errno not in (errno.ENOPROTOOPT, errno.EINVAL):\n                    raise\n        if not bound:\n            self.bind(sock)\n        sock.setblocking(0)\n\n        # make sure that the socket can be inherited\n        if hasattr(sock, \"set_inheritable\"):\n            sock.set_inheritable(True)\n\n        sock.listen(self.conf.backlog)\n        return sock\n\n    def bind(self, sock):\n        sock.bind(self.cfg_addr)\n\n    def close(self):\n        if self.sock is None:\n            return\n\n        try:\n            self.sock.close()\n        except OSError as e:\n            self.log.info(\"Error while closing socket %s\", str(e))\n\n        self.sock = None\n\n    def get_backlog(self):\n        return -1\n\n\nclass TCPSocket(BaseSocket):\n\n    FAMILY = socket.AF_INET\n\n    def __str__(self):\n        if self.conf.is_ssl:\n            scheme = \"https\"\n        else:\n            scheme = \"http\"\n\n        addr = self.sock.getsockname()\n        return \"%s://%s:%d\" % (scheme, addr[0], addr[1])\n\n    def set_options(self, sock, bound=False):\n        sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)\n        return super().set_options(sock, bound=bound)\n\n    if PLATFORM == \"linux\":\n        def get_backlog(self):\n            if self.sock:\n                # tcp_info struct from include/uapi/linux/tcp.h\n                fmt = 'B' * 8 + 'I' * 24\n                try:\n                    tcp_info_struct = self.sock.getsockopt(socket.IPPROTO_TCP,\n                                                           socket.TCP_INFO, 104)\n                    # 12 is tcpi_unacked\n                    return struct.unpack(fmt, tcp_info_struct)[12]\n                except (AttributeError, OSError):\n                    pass\n            return 0\n    else:\n        def get_backlog(self):\n            return -1\n\n\nclass TCP6Socket(TCPSocket):\n\n    FAMILY = socket.AF_INET6\n\n    def __str__(self):\n        (host, port, _, _) = self.sock.getsockname()\n        return \"http://[%s]:%d\" % (host, port)\n\n\nclass UnixSocket(BaseSocket):\n\n    FAMILY = socket.AF_UNIX\n\n    def __init__(self, addr, conf, log, fd=None):\n        if fd is None:\n            try:\n                st = os.stat(addr)\n            except OSError as e:\n                if e.args[0] != errno.ENOENT:\n                    raise\n            else:\n                if stat.S_ISSOCK(st.st_mode):\n                    os.remove(addr)\n                else:\n                    raise ValueError(\"%r is not a socket\" % addr)\n        super().__init__(addr, conf, log, fd=fd)\n\n    def __str__(self):\n        return \"unix:%s\" % self.cfg_addr\n\n    def bind(self, sock):\n        old_umask = os.umask(self.conf.umask)\n        sock.bind(self.cfg_addr)\n        util.chown(self.cfg_addr, self.conf.uid, self.conf.gid)\n        os.umask(old_umask)\n\n\ndef _sock_type(addr):\n    if isinstance(addr, tuple):\n        if util.is_ipv6(addr[0]):\n            sock_type = TCP6Socket\n        else:\n            sock_type = TCPSocket\n    elif isinstance(addr, (str, bytes)):\n        sock_type = UnixSocket\n    else:\n        raise TypeError(\"Unable to create socket from: %r\" % addr)\n    return sock_type\n\n\ndef create_sockets(conf, log, fds=None):\n    \"\"\"\n    Create a new socket for the configured addresses or file descriptors.\n\n    If a configured address is a tuple then a TCP socket is created.\n    If it is a string, a Unix socket is created. Otherwise, a TypeError is\n    raised.\n    \"\"\"\n    listeners = []\n\n    # get it only once\n    addr = conf.address\n    fdaddr = [bind for bind in addr if isinstance(bind, int)]\n    if fds:\n        fdaddr += list(fds)\n    laddr = [bind for bind in addr if not isinstance(bind, int)]\n\n    # check ssl config early to raise the error on startup\n    # only the certfile is needed since it can contains the keyfile\n    if conf.certfile and not os.path.exists(conf.certfile):\n        raise ValueError('certfile \"%s\" does not exist' % conf.certfile)\n\n    if conf.keyfile and not os.path.exists(conf.keyfile):\n        raise ValueError('keyfile \"%s\" does not exist' % conf.keyfile)\n\n    # sockets are already bound\n    if fdaddr:\n        for fd in fdaddr:\n            sock = socket.fromfd(fd, socket.AF_UNIX, socket.SOCK_STREAM)\n            sock_name = sock.getsockname()\n            sock_type = _sock_type(sock_name)\n            listener = sock_type(sock_name, conf, log, fd=fd)\n            listeners.append(listener)\n\n        return listeners\n\n    # no sockets is bound, first initialization of gunicorn in this env.\n    for addr in laddr:\n        sock_type = _sock_type(addr)\n        sock = None\n        for i in range(5):\n            try:\n                sock = sock_type(addr, conf, log)\n            except OSError as e:\n                if e.args[0] == errno.EADDRINUSE:\n                    log.error(\"Connection in use: %s\", str(addr))\n                if e.args[0] == errno.EADDRNOTAVAIL:\n                    log.error(\"Invalid address: %s\", str(addr))\n                msg = \"connection to {addr} failed: {error}\"\n                log.error(msg.format(addr=str(addr), error=str(e)))\n                if i < 5:\n                    log.debug(\"Retrying in 1 second.\")\n                    time.sleep(1)\n            else:\n                break\n\n        if sock is None:\n            log.error(\"Can't connect to %s\", str(addr))\n            sys.exit(1)\n\n        listeners.append(sock)\n\n    return listeners\n\n\ndef close_sockets(listeners, unlink=True):\n    for sock in listeners:\n        sock_name = sock.getsockname()\n        sock.close()\n        if unlink and _sock_type(sock_name) is UnixSocket:\n            os.unlink(sock_name)\n\n\ndef _get_alpn_protocols(conf):\n    \"\"\"Get ALPN protocol list from configuration.\n\n    Returns list of ALPN protocol identifiers based on http_protocols setting.\n    Returns empty list if HTTP/2 is not configured or available.\n    \"\"\"\n    from gunicorn.config import ALPN_PROTOCOL_MAP\n\n    http_protocols = conf.http_protocols\n    if not http_protocols:\n        return []\n\n    # Only configure ALPN if h2 is in the protocol list\n    if \"h2\" not in http_protocols:\n        return []\n\n    # Check if h2 library is available\n    from gunicorn.http2 import is_http2_available\n    if not is_http2_available():\n        return []\n\n    # Map to ALPN identifiers, maintaining preference order\n    alpn_protocols = []\n    for proto in http_protocols:\n        if proto in ALPN_PROTOCOL_MAP:\n            alpn_protocols.append(ALPN_PROTOCOL_MAP[proto])\n    return alpn_protocols\n\n\ndef ssl_context(conf):\n    def default_ssl_context_factory():\n        context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH, cafile=conf.ca_certs)\n        context.load_cert_chain(certfile=conf.certfile, keyfile=conf.keyfile)\n        context.verify_mode = conf.cert_reqs\n        if conf.ciphers:\n            context.set_ciphers(conf.ciphers)\n\n        # Configure ALPN for HTTP/2 if enabled\n        alpn_protocols = _get_alpn_protocols(conf)\n        if alpn_protocols:\n            context.set_alpn_protocols(alpn_protocols)\n\n        return context\n\n    return conf.ssl_context(conf, default_ssl_context_factory)\n\n\ndef ssl_wrap_socket(sock, conf):\n    return ssl_context(conf).wrap_socket(sock,\n                                         server_side=True,\n                                         suppress_ragged_eofs=conf.suppress_ragged_eofs,\n                                         do_handshake_on_connect=conf.do_handshake_on_connect)\n\n\ndef get_negotiated_protocol(ssl_socket):\n    \"\"\"Get the negotiated ALPN protocol from an SSL socket.\n\n    Returns:\n        str: The negotiated protocol name ('h2', 'http/1.1', etc.)\n             or None if no protocol was negotiated.\n    \"\"\"\n    if not isinstance(ssl_socket, ssl.SSLSocket):\n        return None\n\n    try:\n        return ssl_socket.selected_alpn_protocol()\n    except (AttributeError, ssl.SSLError):\n        return None\n\n\ndef is_http2_negotiated(ssl_socket):\n    \"\"\"Check if HTTP/2 was negotiated on an SSL socket.\n\n    Returns:\n        bool: True if HTTP/2 was negotiated via ALPN.\n    \"\"\"\n    protocol = get_negotiated_protocol(ssl_socket)\n    return protocol == \"h2\"\n"
  },
  {
    "path": "gunicorn/systemd.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport os\nimport socket\n\nSD_LISTEN_FDS_START = 3\n\n\ndef listen_fds(unset_environment=True):\n    \"\"\"\n    Get the number of sockets inherited from systemd socket activation.\n\n    :param unset_environment: clear systemd environment variables unless False\n    :type unset_environment: bool\n    :return: the number of sockets to inherit from systemd socket activation\n    :rtype: int\n\n    Returns zero immediately if $LISTEN_PID is not set to the current pid.\n    Otherwise, returns the number of systemd activation sockets specified by\n    $LISTEN_FDS.\n\n    When $LISTEN_PID matches the current pid, unsets the environment variables\n    unless the ``unset_environment`` flag is ``False``.\n\n    .. note::\n        Unlike the sd_listen_fds C function, this implementation does not set\n        the FD_CLOEXEC flag because the gunicorn arbiter never needs to do this.\n\n    .. seealso::\n        `<https://www.freedesktop.org/software/systemd/man/sd_listen_fds.html>`_\n\n    \"\"\"\n    fds = int(os.environ.get('LISTEN_FDS', 0))\n    listen_pid = int(os.environ.get('LISTEN_PID', 0))\n\n    if listen_pid != os.getpid():\n        return 0\n\n    if unset_environment:\n        os.environ.pop('LISTEN_PID', None)\n        os.environ.pop('LISTEN_FDS', None)\n\n    return fds\n\n\ndef sd_notify(state, logger, unset_environment=False):\n    \"\"\"Send a notification to systemd. state is a string; see\n    the man page of sd_notify (http://www.freedesktop.org/software/systemd/man/sd_notify.html)\n    for a description of the allowable values.\n\n    If the unset_environment parameter is True, sd_notify() will unset\n    the $NOTIFY_SOCKET environment variable before returning (regardless of\n    whether the function call itself succeeded or not). Further calls to\n    sd_notify() will then fail, but the variable is no longer inherited by\n    child processes.\n    \"\"\"\n\n    addr = os.environ.get('NOTIFY_SOCKET')\n    if addr is None:\n        # not run in a service, just a noop\n        return\n    sock = None\n    try:\n        sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM | socket.SOCK_CLOEXEC)\n        if addr[0] == '@':\n            addr = '\\0' + addr[1:]\n        sock.connect(addr)\n        sock.sendall(state.encode('utf-8'))\n    except Exception:\n        logger.debug(\"Exception while invoking sd_notify()\", exc_info=True)\n    finally:\n        if unset_environment:\n            os.environ.pop('NOTIFY_SOCKET')\n        if sock is not None:\n            sock.close()\n"
  },
  {
    "path": "gunicorn/util.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\nimport ast\nimport email.utils\nimport errno\nimport fcntl\nimport html\nimport importlib\nimport inspect\nimport io\nimport logging\nimport os\nimport pwd\nimport random\nimport re\nimport socket\nimport sys\nimport textwrap\nimport time\nimport traceback\nimport warnings\n\ntry:\n    import importlib.metadata as importlib_metadata\nexcept (ModuleNotFoundError, ImportError):\n    import importlib_metadata\n\nfrom gunicorn.errors import AppImportError\nfrom gunicorn.workers import SUPPORTED_WORKERS\nimport urllib.parse\n\nREDIRECT_TO = getattr(os, 'devnull', '/dev/null')\n\n# Server and Date aren't technically hop-by-hop\n# headers, but they are in the purview of the\n# origin server which the WSGI spec says we should\n# act like. So we drop them and add our own.\n#\n# In the future, concatenation server header values\n# might be better, but nothing else does it and\n# dropping them is easier.\nhop_headers = set(\"\"\"\n    connection keep-alive proxy-authenticate proxy-authorization\n    te trailers transfer-encoding upgrade\n    server date\n    \"\"\".split())\n\n# setproctitle causes segfaults on macOS due to fork() safety issues\n# https://github.com/benoitc/gunicorn/issues/3021\nif sys.platform == \"darwin\":\n    def _setproctitle(title):\n        pass\nelse:\n    try:\n        from setproctitle import setproctitle, getproctitle\n\n        # Force early initialization before any os.environ modifications\n        # (e.g. removing LISTEN_FDS in systemd socket activation)\n        # https://github.com/benoitc/gunicorn/issues/3430\n        getproctitle()\n\n        def _setproctitle(title):\n            setproctitle(\"gunicorn: %s\" % title)\n    except ImportError:\n        def _setproctitle(title):\n            pass\n\n\ndef load_entry_point(distribution, group, name):\n    dist_obj = importlib_metadata.distribution(distribution)\n    eps = [ep for ep in dist_obj.entry_points\n           if ep.group == group and ep.name == name]\n    if not eps:\n        raise ImportError(\"Entry point %r not found\" % ((group, name),))\n    return eps[0].load()\n\n\ndef load_class(uri, default=\"gunicorn.workers.sync.SyncWorker\",\n               section=\"gunicorn.workers\"):\n    if inspect.isclass(uri):\n        return uri\n    if uri.startswith(\"egg:\"):\n        # uses entry points\n        entry_str = uri.split(\"egg:\")[1]\n        try:\n            dist, name = entry_str.rsplit(\"#\", 1)\n        except ValueError:\n            dist = entry_str\n            name = default\n\n        try:\n            return load_entry_point(dist, section, name)\n        except Exception:\n            exc = traceback.format_exc()\n            msg = \"class uri %r invalid or not found: \\n\\n[%s]\"\n            raise RuntimeError(msg % (uri, exc))\n    else:\n        components = uri.split('.')\n        if len(components) == 1:\n            while True:\n                if uri.startswith(\"#\"):\n                    uri = uri[1:]\n\n                if uri in SUPPORTED_WORKERS:\n                    components = SUPPORTED_WORKERS[uri].split(\".\")\n                    break\n\n                try:\n                    return load_entry_point(\n                        \"gunicorn\", section, uri\n                    )\n                except Exception:\n                    exc = traceback.format_exc()\n                    msg = \"class uri %r invalid or not found: \\n\\n[%s]\"\n                    raise RuntimeError(msg % (uri, exc))\n\n        klass = components.pop(-1)\n\n        try:\n            mod = importlib.import_module('.'.join(components))\n        except Exception:\n            exc = traceback.format_exc()\n            msg = \"class uri %r invalid or not found: \\n\\n[%s]\"\n            raise RuntimeError(msg % (uri, exc))\n        return getattr(mod, klass)\n\n\npositionals = (\n    inspect.Parameter.POSITIONAL_ONLY,\n    inspect.Parameter.POSITIONAL_OR_KEYWORD,\n)\n\n\ndef get_arity(f):\n    sig = inspect.signature(f)\n    arity = 0\n\n    for param in sig.parameters.values():\n        if param.kind in positionals:\n            arity += 1\n\n    return arity\n\n\ndef get_username(uid):\n    \"\"\" get the username for a user id\"\"\"\n    return pwd.getpwuid(uid).pw_name\n\n\ndef set_owner_process(uid, gid, initgroups=False):\n    \"\"\" set user and group of workers processes \"\"\"\n\n    if gid:\n        if uid:\n            try:\n                username = get_username(uid)\n            except KeyError:\n                initgroups = False\n\n        if initgroups:\n            os.initgroups(username, gid)\n        elif gid != os.getgid():\n            os.setgid(gid)\n\n    if uid and uid != os.getuid():\n        os.setuid(uid)\n\n\ndef chown(path, uid, gid):\n    os.chown(path, uid, gid)\n\n\nif sys.platform.startswith(\"win\"):\n    def _waitfor(func, pathname, waitall=False):\n        # Perform the operation\n        func(pathname)\n        # Now setup the wait loop\n        if waitall:\n            dirname = pathname\n        else:\n            dirname, name = os.path.split(pathname)\n            dirname = dirname or '.'\n        # Check for `pathname` to be removed from the filesystem.\n        # The exponential backoff of the timeout amounts to a total\n        # of ~1 second after which the deletion is probably an error\n        # anyway.\n        # Testing on a i7@4.3GHz shows that usually only 1 iteration is\n        # required when contention occurs.\n        timeout = 0.001\n        while timeout < 1.0:\n            # Note we are only testing for the existence of the file(s) in\n            # the contents of the directory regardless of any security or\n            # access rights.  If we have made it this far, we have sufficient\n            # permissions to do that much using Python's equivalent of the\n            # Windows API FindFirstFile.\n            # Other Windows APIs can fail or give incorrect results when\n            # dealing with files that are pending deletion.\n            L = os.listdir(dirname)\n            if not L if waitall else name in L:\n                return\n            # Increase the timeout and try again\n            time.sleep(timeout)\n            timeout *= 2\n        warnings.warn('tests may fail, delete still pending for ' + pathname,\n                      RuntimeWarning, stacklevel=4)\n\n    def _unlink(filename):\n        _waitfor(os.unlink, filename)\nelse:\n    _unlink = os.unlink\n\n\ndef unlink(filename):\n    try:\n        _unlink(filename)\n    except OSError as error:\n        # The filename need not exist.\n        if error.errno not in (errno.ENOENT, errno.ENOTDIR):\n            raise\n\n\ndef is_ipv6(addr):\n    try:\n        socket.inet_pton(socket.AF_INET6, addr)\n    except OSError:  # not a valid address\n        return False\n    except ValueError:  # ipv6 not supported on this platform\n        return False\n    return True\n\n\ndef parse_address(netloc, default_port='8000'):\n    if re.match(r'unix:(//)?', netloc):\n        return re.split(r'unix:(//)?', netloc)[-1]\n\n    if netloc.startswith(\"fd://\"):\n        fd = netloc[5:]\n        try:\n            return int(fd)\n        except ValueError:\n            raise RuntimeError(\"%r is not a valid file descriptor.\" % fd) from None\n\n    if netloc.startswith(\"tcp://\"):\n        netloc = netloc.split(\"tcp://\")[1]\n    host, port = netloc, default_port\n\n    if '[' in netloc and ']' in netloc:\n        host = netloc.split(']')[0][1:]\n        port = (netloc.split(']:') + [default_port])[1]\n    elif ':' in netloc:\n        host, port = (netloc.split(':') + [default_port])[:2]\n    elif netloc == \"\":\n        host, port = \"0.0.0.0\", default_port\n\n    try:\n        port = int(port)\n    except ValueError:\n        raise RuntimeError(\"%r is not a valid port number.\" % port)\n\n    return host.lower(), port\n\n\ndef close_on_exec(fd):\n    flags = fcntl.fcntl(fd, fcntl.F_GETFD)\n    flags |= fcntl.FD_CLOEXEC\n    fcntl.fcntl(fd, fcntl.F_SETFD, flags)\n\n\ndef set_non_blocking(fd):\n    flags = fcntl.fcntl(fd, fcntl.F_GETFL) | os.O_NONBLOCK\n    fcntl.fcntl(fd, fcntl.F_SETFL, flags)\n\n\ndef close(sock):\n    try:\n        sock.close()\n    except OSError:\n        pass\n\n\ntry:\n    from os import closerange\nexcept ImportError:\n    def closerange(fd_low, fd_high):\n        # Iterate through and close all file descriptors.\n        for fd in range(fd_low, fd_high):\n            try:\n                os.close(fd)\n            except OSError:  # ERROR, fd wasn't open to begin with (ignored)\n                pass\n\n\ndef write_chunk(sock, data):\n    if isinstance(data, str):\n        data = data.encode('utf-8')\n    chunk_size = \"%X\\r\\n\" % len(data)\n    chunk = b\"\".join([chunk_size.encode('utf-8'), data, b\"\\r\\n\"])\n    sock.sendall(chunk)\n\n\ndef write(sock, data, chunked=False):\n    if chunked:\n        return write_chunk(sock, data)\n    sock.sendall(data)\n\n\ndef write_nonblock(sock, data, chunked=False):\n    timeout = sock.gettimeout()\n    if timeout != 0.0:\n        try:\n            sock.setblocking(0)\n            return write(sock, data, chunked)\n        finally:\n            sock.setblocking(1)\n    else:\n        return write(sock, data, chunked)\n\n\ndef write_error(sock, status_int, reason, mesg):\n    html_error = textwrap.dedent(\"\"\"\\\n    <html>\n      <head>\n        <title>%(reason)s</title>\n      </head>\n      <body>\n        <h1><p>%(reason)s</p></h1>\n        %(mesg)s\n      </body>\n    </html>\n    \"\"\") % {\"reason\": reason, \"mesg\": html.escape(mesg)}\n\n    http = textwrap.dedent(\"\"\"\\\n    HTTP/1.1 %s %s\\r\n    Connection: close\\r\n    Content-Type: text/html\\r\n    Content-Length: %d\\r\n    \\r\n    %s\"\"\") % (str(status_int), reason, len(html_error), html_error)\n    write_nonblock(sock, http.encode('latin1'))\n\n\ndef _called_with_wrong_args(f):\n    \"\"\"Check whether calling a function raised a ``TypeError`` because\n    the call failed or because something in the function raised the\n    error.\n\n    :param f: The function that was called.\n    :return: ``True`` if the call failed.\n    \"\"\"\n    tb = sys.exc_info()[2]\n\n    try:\n        while tb is not None:\n            if tb.tb_frame.f_code is f.__code__:\n                # In the function, it was called successfully.\n                return False\n\n            tb = tb.tb_next\n\n        # Didn't reach the function.\n        return True\n    finally:\n        # Delete tb to break a circular reference in Python 2.\n        # https://docs.python.org/2/library/sys.html#sys.exc_info\n        del tb\n\n\ndef import_app(module):\n    parts = module.split(\":\", 1)\n    if len(parts) == 1:\n        obj = \"application\"\n    else:\n        module, obj = parts[0], parts[1]\n\n    try:\n        mod = importlib.import_module(module)\n    except ImportError:\n        if module.endswith(\".py\") and os.path.exists(module):\n            msg = \"Failed to find application, did you mean '%s:%s'?\"\n            raise ImportError(msg % (module.rsplit(\".\", 1)[0], obj))\n        raise\n\n    # Parse obj as a single expression to determine if it's a valid\n    # attribute name or function call.\n    try:\n        expression = ast.parse(obj, mode=\"eval\").body\n    except SyntaxError:\n        raise AppImportError(\n            \"Failed to parse %r as an attribute name or function call.\" % obj\n        )\n\n    if isinstance(expression, ast.Name):\n        name = expression.id\n        args = kwargs = None\n    elif isinstance(expression, ast.Call):\n        # Ensure the function name is an attribute name only.\n        if not isinstance(expression.func, ast.Name):\n            raise AppImportError(\"Function reference must be a simple name: %r\" % obj)\n\n        name = expression.func.id\n\n        # Parse the positional and keyword arguments as literals.\n        try:\n            args = [ast.literal_eval(arg) for arg in expression.args]\n            kwargs = {kw.arg: ast.literal_eval(kw.value) for kw in expression.keywords}\n        except ValueError:\n            # literal_eval gives cryptic error messages, show a generic\n            # message with the full expression instead.\n            raise AppImportError(\n                \"Failed to parse arguments as literal values: %r\" % obj\n            )\n    else:\n        raise AppImportError(\n            \"Failed to parse %r as an attribute name or function call.\" % obj\n        )\n\n    is_debug = logging.root.level == logging.DEBUG\n    try:\n        app = getattr(mod, name)\n    except AttributeError:\n        if is_debug:\n            traceback.print_exception(*sys.exc_info())\n        raise AppImportError(\"Failed to find attribute %r in %r.\" % (name, module))\n\n    # If the expression was a function call, call the retrieved object\n    # to get the real application.\n    if args is not None:\n        try:\n            app = app(*args, **kwargs)\n        except TypeError as e:\n            # If the TypeError was due to bad arguments to the factory\n            # function, show Python's nice error message without a\n            # traceback.\n            if _called_with_wrong_args(app):\n                raise AppImportError(\n                    \"\".join(traceback.format_exception_only(TypeError, e)).strip()\n                )\n\n            # Otherwise it was raised from within the function, show the\n            # full traceback.\n            raise\n\n    if app is None:\n        raise AppImportError(\"Failed to find application object: %r\" % obj)\n\n    if not callable(app):\n        raise AppImportError(\"Application object must be callable.\")\n    return app\n\n\ndef getcwd():\n    # get current path, try to use PWD env first\n    try:\n        a = os.stat(os.environ['PWD'])\n        b = os.stat(os.getcwd())\n        if a.st_ino == b.st_ino and a.st_dev == b.st_dev:\n            cwd = os.environ['PWD']\n        else:\n            cwd = os.getcwd()\n    except Exception:\n        cwd = os.getcwd()\n    return cwd\n\n\ndef http_date(timestamp=None):\n    \"\"\"Return the current date and time formatted for a message header.\"\"\"\n    if timestamp is None:\n        timestamp = time.time()\n    s = email.utils.formatdate(timestamp, localtime=False, usegmt=True)\n    return s\n\n\ndef is_hoppish(header):\n    return header.lower().strip() in hop_headers\n\n\ndef daemonize(enable_stdio_inheritance=False):\n    \"\"\"\\\n    Standard daemonization of a process.\n    http://www.faqs.org/faqs/unix-faq/programmer/faq/ section 1.7\n    \"\"\"\n    if 'GUNICORN_FD' not in os.environ:\n        if os.fork():\n            os._exit(0)\n        os.setsid()\n\n        if os.fork():\n            os._exit(0)\n\n        os.umask(0o22)\n\n        # In both the following any file descriptors above stdin\n        # stdout and stderr are left untouched. The inheritance\n        # option simply allows one to have output go to a file\n        # specified by way of shell redirection when not wanting\n        # to use --error-log option.\n\n        if not enable_stdio_inheritance:\n            # Remap all of stdin, stdout and stderr on to\n            # /dev/null. The expectation is that users have\n            # specified the --error-log option.\n\n            closerange(0, 3)\n\n            fd_null = os.open(REDIRECT_TO, os.O_RDWR)\n            # PEP 446, make fd for /dev/null inheritable\n            os.set_inheritable(fd_null, True)\n\n            # expect fd_null to be always 0 here, but in-case not ...\n            if fd_null != 0:\n                os.dup2(fd_null, 0)\n\n            os.dup2(fd_null, 1)\n            os.dup2(fd_null, 2)\n\n        else:\n            fd_null = os.open(REDIRECT_TO, os.O_RDWR)\n\n            # Always redirect stdin to /dev/null as we would\n            # never expect to need to read interactive input.\n\n            if fd_null != 0:\n                os.close(0)\n                os.dup2(fd_null, 0)\n\n            # If stdout and stderr are still connected to\n            # their original file descriptors we check to see\n            # if they are associated with terminal devices.\n            # When they are we map them to /dev/null so that\n            # are still detached from any controlling terminal\n            # properly. If not we preserve them as they are.\n            #\n            # If stdin and stdout were not hooked up to the\n            # original file descriptors, then all bets are\n            # off and all we can really do is leave them as\n            # they were.\n            #\n            # This will allow 'gunicorn ... > output.log 2>&1'\n            # to work with stdout/stderr going to the file\n            # as expected.\n            #\n            # Note that if using --error-log option, the log\n            # file specified through shell redirection will\n            # only be used up until the log file specified\n            # by the option takes over. As it replaces stdout\n            # and stderr at the file descriptor level, then\n            # anything using stdout or stderr, including having\n            # cached a reference to them, will still work.\n\n            def redirect(stream, fd_expect):\n                try:\n                    fd = stream.fileno()\n                    if fd == fd_expect and stream.isatty():\n                        os.close(fd)\n                        os.dup2(fd_null, fd)\n                except AttributeError:\n                    pass\n\n            redirect(sys.stdout, 1)\n            redirect(sys.stderr, 2)\n\n\ndef seed():\n    try:\n        random.seed(os.urandom(64))\n    except NotImplementedError:\n        random.seed('%s.%s' % (time.time(), os.getpid()))\n\n\ndef check_is_writable(path):\n    try:\n        with open(path, 'a') as f:\n            f.close()\n    except OSError as e:\n        raise RuntimeError(\"Error: '%s' isn't writable [%r]\" % (path, e))\n\n\ndef to_bytestring(value, encoding=\"utf8\"):\n    \"\"\"Converts a string argument to a byte string\"\"\"\n    if isinstance(value, bytes):\n        return value\n    if not isinstance(value, str):\n        raise TypeError('%r is not a string' % value)\n\n    return value.encode(encoding)\n\n\ndef has_fileno(obj):\n    if not hasattr(obj, \"fileno\"):\n        return False\n\n    # check BytesIO case and maybe others\n    try:\n        obj.fileno()\n    except (AttributeError, OSError, io.UnsupportedOperation):\n        return False\n\n    return True\n\n\ndef warn(msg):\n    print(\"!!!\", file=sys.stderr)\n\n    lines = msg.splitlines()\n    for i, line in enumerate(lines):\n        if i == 0:\n            line = \"WARNING: %s\" % line\n        print(\"!!! %s\" % line, file=sys.stderr)\n\n    print(\"!!!\\n\", file=sys.stderr)\n    sys.stderr.flush()\n\n\ndef make_fail_app(msg):\n    msg = to_bytestring(msg)\n\n    def app(environ, start_response):\n        start_response(\"500 Internal Server Error\", [\n            (\"Content-Type\", \"text/plain\"),\n            (\"Content-Length\", str(len(msg)))\n        ])\n        return [msg]\n\n    return app\n\n\ndef split_request_uri(uri):\n    if uri.startswith(\"//\"):\n        # When the path starts with //, urlsplit considers it as a\n        # relative uri while the RFC says we should consider it as abs_path\n        # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2\n        # We use temporary dot prefix to workaround this behaviour\n        parts = urllib.parse.urlsplit(\".\" + uri)\n        return parts._replace(path=parts.path[1:])\n\n    return urllib.parse.urlsplit(uri)\n\n\n# From six.reraise\ndef reraise(tp, value, tb=None):\n    try:\n        if value is None:\n            value = tp()\n        if value.__traceback__ is not tb:\n            raise value.with_traceback(tb)\n        raise value\n    finally:\n        value = None\n        tb = None\n\n\ndef bytes_to_str(b):\n    if isinstance(b, str):\n        return b\n    return str(b, 'latin1')\n\n\ndef unquote_to_wsgi_str(string):\n    return urllib.parse.unquote_to_bytes(string).decode('latin-1')\n"
  },
  {
    "path": "gunicorn/uwsgi/__init__.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.uwsgi.message import UWSGIRequest\nfrom gunicorn.uwsgi.parser import UWSGIParser\nfrom gunicorn.uwsgi.errors import (\n    UWSGIParseException,\n    InvalidUWSGIHeader,\n    UnsupportedModifier,\n    ForbiddenUWSGIRequest,\n)\n\n__all__ = [\n    'UWSGIRequest',\n    'UWSGIParser',\n    'UWSGIParseException',\n    'InvalidUWSGIHeader',\n    'UnsupportedModifier',\n    'ForbiddenUWSGIRequest',\n]\n"
  },
  {
    "path": "gunicorn/uwsgi/errors.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n# We don't need to call super() in __init__ methods of our\n# BaseException and Exception classes because we also define\n# our own __str__ methods so there is no need to pass 'message'\n# to the base class to get a meaningful output from 'str(exc)'.\n# pylint: disable=super-init-not-called\n\n\nclass UWSGIParseException(Exception):\n    \"\"\"Base exception for uWSGI protocol parsing errors.\"\"\"\n\n\nclass InvalidUWSGIHeader(UWSGIParseException):\n    \"\"\"Raised when the uWSGI header is malformed.\"\"\"\n\n    def __init__(self, msg=\"\"):\n        self.msg = msg\n        self.code = 400\n\n    def __str__(self):\n        return \"Invalid uWSGI header: %s\" % self.msg\n\n\nclass UnsupportedModifier(UWSGIParseException):\n    \"\"\"Raised when modifier1 is not 0 (WSGI request).\"\"\"\n\n    def __init__(self, modifier):\n        self.modifier = modifier\n        self.code = 501\n\n    def __str__(self):\n        return \"Unsupported uWSGI modifier1: %d\" % self.modifier\n\n\nclass ForbiddenUWSGIRequest(UWSGIParseException):\n    \"\"\"Raised when source IP is not in the allow list.\"\"\"\n\n    def __init__(self, host):\n        self.host = host\n        self.code = 403\n\n    def __str__(self):\n        return \"uWSGI request from %r not allowed\" % self.host\n"
  },
  {
    "path": "gunicorn/uwsgi/message.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport io\n\nfrom gunicorn.http.body import LengthReader, Body\nfrom gunicorn.uwsgi.errors import (\n    InvalidUWSGIHeader,\n    UnsupportedModifier,\n    ForbiddenUWSGIRequest,\n)\n\n\n# Maximum number of variables to prevent DoS\nMAX_UWSGI_VARS = 1000\n\n\nclass UWSGIRequest:\n    \"\"\"uWSGI protocol request parser.\n\n    The uWSGI protocol uses a 4-byte binary header:\n    - Byte 0: modifier1 (packet type, 0 = WSGI request)\n    - Bytes 1-2: datasize (16-bit little-endian, size of vars block)\n    - Byte 3: modifier2 (additional flags, typically 0)\n\n    After the header:\n    1. Vars block (datasize bytes): Key-value pairs containing WSGI environ\n       - Each pair: 2-byte key_size (LE) + key + 2-byte val_size (LE) + value\n    2. Request body (determined by CONTENT_LENGTH in vars)\n    \"\"\"\n\n    def __init__(self, cfg, unreader, peer_addr, req_number=1):\n        self.cfg = cfg\n        self.unreader = unreader\n        self.peer_addr = peer_addr\n        self.remote_addr = peer_addr\n        self.req_number = req_number\n\n        # Request attributes (compatible with HTTP Request interface)\n        self.method = None\n        self.uri = None\n        self.path = None\n        self.query = None\n        self.fragment = \"\"\n        self.version = (1, 1)  # uWSGI is HTTP/1.1 compatible\n        self.headers = []\n        self.trailers = []\n        self.body = None\n        self.scheme = \"https\" if cfg.is_ssl else \"http\"\n        self.must_close = False\n\n        # uWSGI specific\n        self.uwsgi_vars = {}\n        self.modifier1 = 0\n        self.modifier2 = 0\n\n        # Proxy protocol compatibility\n        self.proxy_protocol_info = None\n\n        # 100-continue: not applicable for uWSGI as the frontend handles this\n        self._expected_100_continue = False\n\n        # Check if the source IP is allowed\n        self._check_allowed_ip()\n\n        # Parse the request\n        unused = self.parse(self.unreader)\n        self.unreader.unread(unused)\n        self.set_body_reader()\n\n    def _check_allowed_ip(self):\n        \"\"\"Verify source IP is in the allowed list.\"\"\"\n        allow_ips = getattr(self.cfg, 'uwsgi_allow_ips', ['127.0.0.1', '::1'])\n\n        # UNIX sockets don't have IP addresses\n        if not isinstance(self.peer_addr, tuple):\n            return\n\n        # Wildcard allows all\n        if '*' in allow_ips:\n            return\n\n        if self.peer_addr[0] not in allow_ips:\n            raise ForbiddenUWSGIRequest(self.peer_addr[0])\n\n    def force_close(self):\n        \"\"\"Force the connection to close after this request.\"\"\"\n        self.must_close = True\n\n    def parse(self, unreader):\n        \"\"\"Parse uWSGI packet header and vars block.\"\"\"\n        # Read the 4-byte header\n        header = self._read_exact(unreader, 4)\n        if len(header) < 4:\n            raise InvalidUWSGIHeader(\"incomplete header\")\n\n        self.modifier1 = header[0]\n        datasize = int.from_bytes(header[1:3], 'little')\n        self.modifier2 = header[3]\n\n        # Only modifier1=0 (WSGI request) is supported\n        if self.modifier1 != 0:\n            raise UnsupportedModifier(self.modifier1)\n\n        # Read the vars block\n        if datasize > 0:\n            vars_data = self._read_exact(unreader, datasize)\n            if len(vars_data) < datasize:\n                raise InvalidUWSGIHeader(\"incomplete vars block\")\n            self._parse_vars(vars_data)\n\n        # Extract HTTP request info from vars\n        self._extract_request_info()\n\n        return b\"\"\n\n    def _read_exact(self, unreader, size):\n        \"\"\"Read exactly size bytes from the unreader.\"\"\"\n        buf = io.BytesIO()\n        remaining = size\n\n        while remaining > 0:\n            data = unreader.read()\n            if not data:\n                break\n            buf.write(data)\n            remaining = size - buf.tell()\n\n        result = buf.getvalue()\n        # Put back any extra bytes\n        if len(result) > size:\n            unreader.unread(result[size:])\n            result = result[:size]\n\n        return result\n\n    def _parse_vars(self, data):\n        \"\"\"Parse uWSGI vars block into key-value pairs.\n\n        Format: key_size (2 bytes LE) + key + val_size (2 bytes LE) + value\n        \"\"\"\n        pos = 0\n        var_count = 0\n\n        while pos < len(data):\n            if var_count >= MAX_UWSGI_VARS:\n                raise InvalidUWSGIHeader(\"too many variables\")\n\n            # Key size (2 bytes, little-endian)\n            if pos + 2 > len(data):\n                raise InvalidUWSGIHeader(\"truncated key size\")\n            key_size = int.from_bytes(data[pos:pos + 2], 'little')\n            pos += 2\n\n            # Key\n            if pos + key_size > len(data):\n                raise InvalidUWSGIHeader(\"truncated key\")\n            key = data[pos:pos + key_size].decode('latin-1')\n            pos += key_size\n\n            # Value size (2 bytes, little-endian)\n            if pos + 2 > len(data):\n                raise InvalidUWSGIHeader(\"truncated value size\")\n            val_size = int.from_bytes(data[pos:pos + 2], 'little')\n            pos += 2\n\n            # Value\n            if pos + val_size > len(data):\n                raise InvalidUWSGIHeader(\"truncated value\")\n            value = data[pos:pos + val_size].decode('latin-1')\n            pos += val_size\n\n            self.uwsgi_vars[key] = value\n            var_count += 1\n\n    def _extract_request_info(self):\n        \"\"\"Extract HTTP request info from uWSGI vars.\n\n        Header Mapping (CGI/WSGI to HTTP):\n\n        The uWSGI protocol passes HTTP headers using CGI-style environment\n        variable naming. This method converts them back to HTTP header format:\n\n        - HTTP_* vars: Strip 'HTTP_' prefix, replace '_' with '-'\n          Example: HTTP_X_FORWARDED_FOR -> X-FORWARDED-FOR\n          Example: HTTP_ACCEPT_ENCODING -> ACCEPT-ENCODING\n\n        - CONTENT_TYPE: Mapped directly to CONTENT-TYPE header\n          (CGI spec excludes HTTP_ prefix for this header)\n\n        - CONTENT_LENGTH: Mapped directly to CONTENT-LENGTH header\n          (CGI spec excludes HTTP_ prefix for this header)\n\n        Note: The underscore-to-hyphen conversion is lossy. Headers that\n        originally contained underscores (e.g., X_Custom_Header) cannot be\n        distinguished from hyphenated headers (X-Custom-Header) after\n        passing through nginx/uWSGI. This is a CGI/WSGI specification\n        limitation, not specific to this implementation.\n        \"\"\"\n        # Method\n        self.method = self.uwsgi_vars.get('REQUEST_METHOD', 'GET')\n\n        # URI and path\n        self.path = self.uwsgi_vars.get('PATH_INFO', '/')\n        self.query = self.uwsgi_vars.get('QUERY_STRING', '')\n\n        # Build URI\n        if self.query:\n            self.uri = \"%s?%s\" % (self.path, self.query)\n        else:\n            self.uri = self.path\n\n        # Scheme\n        if self.uwsgi_vars.get('HTTPS', '').lower() in ('on', '1', 'true'):\n            self.scheme = 'https'\n        elif 'wsgi.url_scheme' in self.uwsgi_vars:\n            self.scheme = self.uwsgi_vars['wsgi.url_scheme']\n\n        # Extract HTTP headers from CGI-style vars\n        # See docstring above for mapping details\n        for key, value in self.uwsgi_vars.items():\n            if key.startswith('HTTP_'):\n                # Convert HTTP_HEADER_NAME to HEADER-NAME\n                header_name = key[5:].replace('_', '-')\n                self.headers.append((header_name, value))\n            elif key == 'CONTENT_TYPE':\n                self.headers.append(('CONTENT-TYPE', value))\n            elif key == 'CONTENT_LENGTH':\n                self.headers.append(('CONTENT-LENGTH', value))\n\n    def set_body_reader(self):\n        \"\"\"Set up the body reader based on CONTENT_LENGTH.\"\"\"\n        content_length = 0\n\n        # Get content length from vars\n        if 'CONTENT_LENGTH' in self.uwsgi_vars:\n            try:\n                content_length = max(int(self.uwsgi_vars['CONTENT_LENGTH']), 0)\n            except ValueError:\n                content_length = 0\n\n        self.body = Body(LengthReader(self.unreader, content_length))\n\n    def should_close(self):\n        \"\"\"Determine if the connection should be closed after this request.\"\"\"\n        if self.must_close:\n            return True\n\n        # Check HTTP_CONNECTION header\n        connection = self.uwsgi_vars.get('HTTP_CONNECTION', '').lower()\n        if connection == 'close':\n            return True\n        elif connection == 'keep-alive':\n            return False\n\n        # Default to keep-alive for HTTP/1.1\n        return False\n"
  },
  {
    "path": "gunicorn/uwsgi/parser.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.parser import Parser\nfrom gunicorn.uwsgi.message import UWSGIRequest\n\n\nclass UWSGIParser(Parser):\n    \"\"\"Parser for uWSGI protocol requests.\"\"\"\n\n    mesg_class = UWSGIRequest\n"
  },
  {
    "path": "gunicorn/workers/__init__.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n# supported gunicorn workers.\nSUPPORTED_WORKERS = {\n    \"sync\": \"gunicorn.workers.sync.SyncWorker\",\n    \"eventlet\": \"gunicorn.workers.geventlet.EventletWorker\",  # DEPRECATED: will be removed in 26.0\n    \"gevent\": \"gunicorn.workers.ggevent.GeventWorker\",\n    \"gevent_wsgi\": \"gunicorn.workers.ggevent.GeventPyWSGIWorker\",\n    \"gevent_pywsgi\": \"gunicorn.workers.ggevent.GeventPyWSGIWorker\",\n    \"tornado\": \"gunicorn.workers.gtornado.TornadoWorker\",\n    \"gthread\": \"gunicorn.workers.gthread.ThreadWorker\",\n    \"asgi\": \"gunicorn.workers.gasgi.ASGIWorker\",\n}\n"
  },
  {
    "path": "gunicorn/workers/base.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport io\nimport os\nimport signal\nimport sys\nimport time\nimport traceback\nfrom datetime import datetime\nfrom random import randint\nfrom ssl import SSLError\n\nfrom gunicorn import util\nfrom gunicorn.http.errors import (\n    ForbiddenProxyRequest, InvalidHeader,\n    InvalidHeaderName, InvalidHTTPVersion,\n    InvalidProxyLine, InvalidRequestLine,\n    InvalidRequestMethod, InvalidSchemeHeaders,\n    LimitRequestHeaders, LimitRequestLine,\n    UnsupportedTransferCoding, ExpectationFailed,\n    ConfigurationProblem, ObsoleteFolding,\n)\nfrom gunicorn.http.wsgi import Response, default_environ\nfrom gunicorn.reloader import reloader_engines\nfrom gunicorn.workers.workertmp import WorkerTmp\n\n\nclass Worker:\n\n    SIGNALS = [getattr(signal, \"SIG%s\" % x) for x in (\n        \"ABRT HUP QUIT INT TERM USR1 USR2 WINCH CHLD\".split()\n    )]\n\n    PIPE = []\n\n    def __init__(self, age, ppid, sockets, app, timeout, cfg, log):\n        \"\"\"\\\n        This is called pre-fork so it shouldn't do anything to the\n        current process. If there's a need to make process wide\n        changes you'll want to do that in ``self.init_process()``.\n        \"\"\"\n        self.age = age\n        self.pid = \"[booting]\"\n        self.ppid = ppid\n        self.sockets = sockets\n        self.app = app\n        self.timeout = timeout\n        self.cfg = cfg\n        self.booted = False\n        self.aborted = False\n        self.reloader = None\n\n        self.nr = 0\n\n        if cfg.max_requests > 0:\n            jitter = randint(0, cfg.max_requests_jitter)\n            self.max_requests = cfg.max_requests + jitter\n        else:\n            self.max_requests = sys.maxsize\n\n        self.alive = True\n        self.log = log\n        self.tmp = WorkerTmp(cfg)\n\n    def __str__(self):\n        return \"<Worker %s>\" % self.pid\n\n    def notify(self):\n        \"\"\"\\\n        Your worker subclass must arrange to have this method called\n        once every ``self.timeout`` seconds. If you fail in accomplishing\n        this task, the master process will murder your workers.\n        \"\"\"\n        self.tmp.notify()\n\n    def run(self):\n        \"\"\"\\\n        This is the mainloop of a worker process. You should override\n        this method in a subclass to provide the intended behaviour\n        for your particular evil schemes.\n        \"\"\"\n        raise NotImplementedError()\n\n    def init_process(self):\n        \"\"\"\\\n        If you override this method in a subclass, the last statement\n        in the function should be to call this method with\n        super().init_process() so that the ``run()`` loop is initiated.\n        \"\"\"\n\n        # set environment' variables\n        if self.cfg.env:\n            for k, v in self.cfg.env.items():\n                os.environ[k] = v\n\n        util.set_owner_process(self.cfg.uid, self.cfg.gid,\n                               initgroups=self.cfg.initgroups)\n\n        # Reseed the random number generator\n        util.seed()\n\n        # For waking ourselves up\n        self.PIPE = os.pipe()\n        for p in self.PIPE:\n            util.set_non_blocking(p)\n            util.close_on_exec(p)\n\n        # Prevent fd inheritance\n        for s in self.sockets:\n            util.close_on_exec(s)\n        util.close_on_exec(self.tmp.fileno())\n\n        self.wait_fds = self.sockets + [self.PIPE[0]]\n\n        self.log.close_on_exec()\n\n        self.init_signals()\n\n        # start the reloader\n        if self.cfg.reload:\n            def changed(fname):\n                self.log.info(\"Worker reloading: %s modified\", fname)\n                self.alive = False\n                os.write(self.PIPE[1], b\"1\")\n                self.cfg.worker_int(self)\n                time.sleep(0.1)\n                sys.exit(0)\n\n            self.log.warning(\"Reloader is on. Use in development only!\")\n            reloader_cls = reloader_engines[self.cfg.reload_engine]\n            self.reloader = reloader_cls(extra_files=self.cfg.reload_extra_files,\n                                         callback=changed)\n\n        self.load_wsgi()\n        if self.reloader:\n            self.reloader.start()\n\n        self.cfg.post_worker_init(self)\n\n        # Enter main run loop\n        self.booted = True\n        self.run()\n\n    def load_wsgi(self):\n        try:\n            self.wsgi = self.app.wsgi()\n        except SyntaxError as e:\n            if not self.cfg.reload:\n                raise\n\n            self.log.exception(e)\n\n            if self.reloader is not None and e.filename is not None:\n                self.reloader.add_extra_file(e.filename)\n\n            with io.StringIO() as tb_string:\n                traceback.print_exception(e, file=tb_string)\n                self.wsgi = util.make_fail_app(tb_string.getvalue())\n\n    def init_signals(self):\n        # reset signaling\n        for s in self.SIGNALS:\n            signal.signal(s, signal.SIG_DFL)\n        # init new signaling\n        signal.signal(signal.SIGQUIT, self.handle_quit)\n        signal.signal(signal.SIGTERM, self.handle_exit)\n        signal.signal(signal.SIGINT, self.handle_quit)\n        signal.signal(signal.SIGWINCH, self.handle_winch)\n        signal.signal(signal.SIGUSR1, self.handle_usr1)\n        signal.signal(signal.SIGABRT, self.handle_abort)\n\n        # Don't let SIGTERM and SIGUSR1 disturb active requests\n        # by interrupting system calls\n        signal.siginterrupt(signal.SIGTERM, False)\n        signal.siginterrupt(signal.SIGUSR1, False)\n\n        if hasattr(signal, 'set_wakeup_fd'):\n            signal.set_wakeup_fd(self.PIPE[1])\n\n    def handle_usr1(self, sig, frame):\n        self.log.reopen_files()\n\n    def handle_exit(self, sig, frame):\n        self.alive = False\n\n    def handle_quit(self, sig, frame):\n        self.alive = False\n        # worker_int callback\n        self.cfg.worker_int(self)\n        time.sleep(0.1)\n        sys.exit(0)\n\n    def handle_abort(self, sig, frame):\n        self.alive = False\n        self.cfg.worker_abort(self)\n        sys.exit(1)\n\n    def handle_error(self, req, client, addr, exc):\n        request_start = datetime.now()\n        addr = addr or ('', -1)  # unix socket case\n        if isinstance(exc, (\n            InvalidRequestLine, InvalidRequestMethod,\n            InvalidHTTPVersion, InvalidHeader, InvalidHeaderName,\n            LimitRequestLine, LimitRequestHeaders,\n            InvalidProxyLine, ForbiddenProxyRequest,\n            InvalidSchemeHeaders, UnsupportedTransferCoding,\n            ConfigurationProblem, ObsoleteFolding, ExpectationFailed,\n            SSLError,\n        )):\n\n            status_int = 400\n            reason = \"Bad Request\"\n\n            if isinstance(exc, InvalidRequestLine):\n                mesg = \"Invalid Request Line '%s'\" % str(exc)\n            elif isinstance(exc, InvalidRequestMethod):\n                mesg = \"Invalid Method '%s'\" % str(exc)\n            elif isinstance(exc, InvalidHTTPVersion):\n                mesg = \"Invalid HTTP Version '%s'\" % str(exc)\n            elif isinstance(exc, UnsupportedTransferCoding):\n                mesg = \"%s\" % str(exc)\n                status_int = 501\n            elif isinstance(exc, ConfigurationProblem):\n                mesg = \"%s\" % str(exc)\n                status_int = 500\n            elif isinstance(exc, ObsoleteFolding):\n                mesg = \"%s\" % str(exc)\n            elif isinstance(exc, (InvalidHeaderName, InvalidHeader,)):\n                mesg = \"%s\" % str(exc)\n                if not req and hasattr(exc, \"req\"):\n                    req = exc.req  # for access log\n            elif isinstance(exc, LimitRequestLine):\n                mesg = \"%s\" % str(exc)\n            elif isinstance(exc, ExpectationFailed):\n                reason = \"Expectation Failed\"\n                mesg = str(exc)\n                status_int = 417\n            elif isinstance(exc, LimitRequestHeaders):\n                reason = \"Request Header Fields Too Large\"\n                mesg = \"Error parsing headers: '%s'\" % str(exc)\n                status_int = 431\n            elif isinstance(exc, InvalidProxyLine):\n                mesg = \"'%s'\" % str(exc)\n            elif isinstance(exc, ForbiddenProxyRequest):\n                reason = \"Forbidden\"\n                mesg = \"Request forbidden\"\n                status_int = 403\n            elif isinstance(exc, InvalidSchemeHeaders):\n                mesg = \"%s\" % str(exc)\n            elif isinstance(exc, SSLError):\n                reason = \"Forbidden\"\n                mesg = \"'%s'\" % str(exc)\n                status_int = 403\n\n            msg = \"Invalid request from ip={ip}: {error}\"\n            self.log.warning(msg.format(ip=addr[0], error=str(exc)))\n        else:\n            if hasattr(req, \"uri\") and hasattr(req, \"method\"):\n                self.log.exception(\"Error handling request %s %s\", req.method, req.uri)\n            elif hasattr(req, \"uri\"):\n                self.log.exception(\"Error handling request %s\", req.uri)\n            else:\n                self.log.exception(\"Error handling request (no URI read)\")\n            status_int = 500\n            reason = \"Internal Server Error\"\n            mesg = \"\"\n\n        if req is not None:\n            request_time = datetime.now() - request_start\n            environ = default_environ(req, client, self.cfg)\n            environ['REMOTE_ADDR'] = addr[0]\n            environ['REMOTE_PORT'] = str(addr[1])\n            resp = Response(req, client, self.cfg)\n            resp.status = \"%s %s\" % (status_int, reason)\n            resp.response_length = len(mesg)\n            self.log.access(resp, req, environ, request_time)\n\n        try:\n            util.write_error(client, status_int, reason, mesg)\n        except Exception:\n            self.log.debug(\"Failed to send error message.\")\n\n    def handle_winch(self, sig, fname):\n        # Ignore SIGWINCH in worker. Fixes a crash on OpenBSD.\n        self.log.debug(\"worker: SIGWINCH ignored.\")\n"
  },
  {
    "path": "gunicorn/workers/base_async.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom datetime import datetime\nimport errno\nimport socket\nimport ssl\nimport sys\n\nfrom gunicorn import http\nfrom gunicorn.http import wsgi\nfrom gunicorn import util\nfrom gunicorn import sock as gunicorn_sock\nfrom gunicorn.workers import base\n\nALREADY_HANDLED = object()\n\n\nclass AsyncWorker(base.Worker):\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.worker_connections = self.cfg.worker_connections\n\n    def timeout_ctx(self):\n        raise NotImplementedError()\n\n    def is_already_handled(self, respiter):\n        # some workers will need to overload this function to raise a StopIteration\n        return respiter == ALREADY_HANDLED\n\n    def handle(self, listener, client, addr):\n        req = None\n        try:\n            # Complete the handshake to ensure ALPN negotiation is done\n            # (needed if do_handshake_on_connect is False)\n            if isinstance(client, ssl.SSLSocket) and not self.cfg.do_handshake_on_connect:\n                client.do_handshake()\n\n            # Check if HTTP/2 was negotiated (for SSL connections)\n            is_http2 = gunicorn_sock.is_http2_negotiated(client)\n\n            if is_http2:\n                # Handle HTTP/2 connection\n                self.handle_http2(listener, client, addr)\n                return\n\n            parser = http.get_parser(self.cfg, client, addr)\n            try:\n                listener_name = listener.getsockname()\n                if not self.cfg.keepalive:\n                    req = next(parser)\n                    self.handle_request(listener_name, req, client, addr)\n                else:\n                    # keepalive loop\n                    proxy_protocol_info = {}\n                    while True:\n                        req = None\n                        with self.timeout_ctx():\n                            req = next(parser)\n                        if not req:\n                            break\n                        if req.proxy_protocol_info:\n                            proxy_protocol_info = req.proxy_protocol_info\n                        else:\n                            req.proxy_protocol_info = proxy_protocol_info\n                        self.handle_request(listener_name, req, client, addr)\n            except http.errors.NoMoreData as e:\n                self.log.debug(\"Ignored premature client disconnection. %s\", e)\n            except StopIteration as e:\n                self.log.debug(\"Closing connection. %s\", e)\n            except ssl.SSLError:\n                # pass to next try-except level\n                util.reraise(*sys.exc_info())\n            except OSError:\n                # pass to next try-except level\n                util.reraise(*sys.exc_info())\n            except Exception as e:\n                self.handle_error(req, client, addr, e)\n        except ssl.SSLError as e:\n            if e.args[0] == ssl.SSL_ERROR_EOF:\n                self.log.debug(\"ssl connection closed\")\n                client.close()\n            else:\n                self.log.debug(\"Error processing SSL request.\")\n                self.handle_error(req, client, addr, e)\n        except OSError as e:\n            if e.errno not in (errno.EPIPE, errno.ECONNRESET, errno.ENOTCONN):\n                self.log.exception(\"Socket error processing request.\")\n            else:\n                if e.errno == errno.ECONNRESET:\n                    self.log.debug(\"Ignoring connection reset\")\n                elif e.errno == errno.ENOTCONN:\n                    self.log.debug(\"Ignoring socket not connected\")\n                else:\n                    self.log.debug(\"Ignoring EPIPE\")\n        except BaseException as e:\n            self.handle_error(req, client, addr, e)\n        finally:\n            util.close(client)\n\n    def handle_http2(self, listener, client, addr):\n        \"\"\"Handle an HTTP/2 connection.\n\n        Processes multiplexed HTTP/2 streams until the connection closes.\n        \"\"\"\n        listener_name = listener.getsockname()\n\n        try:\n            h2_conn = http.get_parser(self.cfg, client, addr, http2_connection=True)\n            h2_conn.initiate_connection()\n\n            while not h2_conn.is_closed and self.alive:\n                try:\n                    requests = h2_conn.receive_data()\n                except http.errors.NoMoreData:\n                    self.log.debug(\"HTTP/2 connection closed by client\")\n                    break\n\n                for req in requests:\n                    try:\n                        self.handle_http2_request(listener_name, req, client, addr, h2_conn)\n                    except Exception as e:\n                        self.log.exception(\"Error handling HTTP/2 request\")\n                        try:\n                            h2_conn.send_error(req.stream.stream_id, 500, str(e))\n                        except Exception:\n                            pass\n                    finally:\n                        h2_conn.cleanup_stream(req.stream.stream_id)\n\n        except ssl.SSLError as e:\n            if e.args[0] == ssl.SSL_ERROR_EOF:\n                self.log.debug(\"HTTP/2 SSL connection closed\")\n            else:\n                self.log.debug(\"HTTP/2 SSL error: %s\", e)\n        except OSError as e:\n            if e.errno not in (errno.EPIPE, errno.ECONNRESET, errno.ENOTCONN):\n                self.log.exception(\"HTTP/2 socket error\")\n        except Exception as e:\n            self.log.exception(\"HTTP/2 connection error: %s\", e)\n\n    def handle_http2_request(self, listener_name, req, sock, addr, h2_conn):\n        \"\"\"Handle a single HTTP/2 request.\"\"\"\n        stream_id = req.stream.stream_id\n        request_start = datetime.now()\n        environ = {}\n        resp = None\n\n        try:\n            self.cfg.pre_request(self, req)\n            resp, environ = wsgi.create(req, sock, addr, listener_name, self.cfg)\n            environ[\"wsgi.multithread\"] = True\n            environ[\"HTTP_VERSION\"] = \"2\"\n\n            self.nr += 1\n            if self.nr >= self.max_requests:\n                if self.alive:\n                    self.log.info(\"Autorestarting worker after current request.\")\n                    self.alive = False\n\n            # Run WSGI app\n            respiter = self.wsgi(environ, resp.start_response)\n            if self.is_already_handled(respiter):\n                return\n\n            # Collect response body\n            response_body = b''\n            try:\n                if hasattr(respiter, '__iter__'):\n                    for item in respiter:\n                        if item:\n                            response_body += item\n            finally:\n                if hasattr(respiter, \"close\"):\n                    respiter.close()\n\n            # Send response via HTTP/2\n            h2_conn.send_response(\n                stream_id,\n                resp.status_code,\n                resp.headers,\n                response_body\n            )\n\n            request_time = datetime.now() - request_start\n            self.log.access(resp, req, environ, request_time)\n\n        except Exception:\n            self.log.exception(\"Error handling HTTP/2 request\")\n            raise\n        finally:\n            try:\n                self.cfg.post_request(self, req, environ, resp)\n            except Exception:\n                self.log.exception(\"Exception in post_request hook\")\n\n    def handle_request(self, listener_name, req, sock, addr):\n        request_start = datetime.now()\n        environ = {}\n        resp = None\n        try:\n            self.cfg.pre_request(self, req)\n            resp, environ = wsgi.create(req, sock, addr,\n                                        listener_name, self.cfg)\n            environ[\"wsgi.multithread\"] = True\n            self.nr += 1\n            if self.nr >= self.max_requests:\n                if self.alive:\n                    self.log.info(\"Autorestarting worker after current request.\")\n                    self.alive = False\n\n            if not self.alive or not self.cfg.keepalive:\n                resp.force_close()\n\n            respiter = self.wsgi(environ, resp.start_response)\n            if self.is_already_handled(respiter):\n                return False\n            try:\n                if isinstance(respiter, environ['wsgi.file_wrapper']):\n                    resp.write_file(respiter)\n                else:\n                    for item in respiter:\n                        resp.write(item)\n                resp.close()\n            finally:\n                request_time = datetime.now() - request_start\n                self.log.access(resp, req, environ, request_time)\n                if hasattr(respiter, \"close\"):\n                    respiter.close()\n            if resp.should_close():\n                raise StopIteration()\n        except StopIteration:\n            raise\n        except OSError:\n            # If the original exception was a socket.error we delegate\n            # handling it to the caller (where handle() might ignore it)\n            util.reraise(*sys.exc_info())\n        except Exception:\n            if resp and resp.headers_sent:\n                # If the requests have already been sent, we should close the\n                # connection to indicate the error.\n                self.log.exception(\"Error handling request\")\n                try:\n                    sock.shutdown(socket.SHUT_RDWR)\n                    sock.close()\n                except OSError:\n                    pass\n                raise StopIteration()\n            raise\n        finally:\n            try:\n                self.cfg.post_request(self, req, environ, resp)\n            except Exception:\n                self.log.exception(\"Exception in post_request hook\")\n        return True\n"
  },
  {
    "path": "gunicorn/workers/gasgi.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nASGI worker for gunicorn.\n\nProvides native asyncio-based ASGI support using gunicorn's own\nHTTP parsing infrastructure.\n\"\"\"\n\nimport asyncio\nimport os\nimport signal\nimport sys\n\nfrom gunicorn.workers import base\nfrom gunicorn.asgi.protocol import ASGIProtocol\n\n\nclass ASGIWorker(base.Worker):\n    \"\"\"ASGI worker using asyncio event loop.\n\n    Supports:\n    - HTTP/1.1 with keepalive\n    - WebSocket connections\n    - Lifespan protocol (startup/shutdown hooks)\n    - Optional uvloop for improved performance\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.worker_connections = self.cfg.worker_connections\n        self.loop = None\n        self.servers = []\n        self.nr_conns = 0\n        self.lifespan = None\n        self.state = {}  # Shared state for lifespan\n        self._quick_shutdown = False  # True for SIGINT/SIGQUIT (immediate), False for SIGTERM (graceful)\n\n    @classmethod\n    def check_config(cls, cfg, log):\n        \"\"\"Validate configuration for ASGI worker.\"\"\"\n        if cfg.threads > 1:\n            log.warning(\"ASGI worker does not use threads configuration. \"\n                        \"Use worker_connections instead.\")\n\n    def init_process(self):\n        \"\"\"Initialize the worker process.\"\"\"\n        # Setup event loop before calling super()\n        self._setup_event_loop()\n        super().init_process()\n\n    def _setup_event_loop(self):\n        \"\"\"Setup the asyncio event loop.\"\"\"\n        loop_type = getattr(self.cfg, 'asgi_loop', 'auto')\n\n        if loop_type == \"auto\":\n            try:\n                import uvloop\n                loop_type = \"uvloop\"\n            except ImportError:\n                loop_type = \"asyncio\"\n\n        if loop_type == \"uvloop\":\n            try:\n                import uvloop\n                self.loop = uvloop.new_event_loop()\n                self.log.debug(\"Using uvloop event loop\")\n            except ImportError:\n                self.log.warning(\"uvloop not available, falling back to asyncio\")\n                self.loop = asyncio.new_event_loop()\n        else:\n            self.loop = asyncio.new_event_loop()\n            self.log.debug(\"Using asyncio event loop\")\n\n        asyncio.set_event_loop(self.loop)\n\n    def load_wsgi(self):\n        \"\"\"Load the ASGI application.\"\"\"\n        try:\n            self.asgi = self.app.wsgi()\n        except SyntaxError as e:\n            if not self.cfg.reload:\n                raise\n            self.log.exception(e)\n            self.asgi = self._make_error_app(str(e))\n\n    def _make_error_app(self, error_msg):\n        \"\"\"Create an error ASGI app for syntax errors during reload.\"\"\"\n        async def error_app(scope, receive, send):\n            if scope[\"type\"] == \"http\":\n                await send({\n                    \"type\": \"http.response.start\",\n                    \"status\": 500,\n                    \"headers\": [(b\"content-type\", b\"text/plain\")],\n                })\n                await send({\n                    \"type\": \"http.response.body\",\n                    \"body\": f\"Application error: {error_msg}\".encode(),\n                })\n            elif scope[\"type\"] == \"lifespan\":\n                message = await receive()\n                if message[\"type\"] == \"lifespan.startup\":\n                    await send({\"type\": \"lifespan.startup.complete\"})\n                message = await receive()\n                if message[\"type\"] == \"lifespan.shutdown\":\n                    await send({\"type\": \"lifespan.shutdown.complete\"})\n        return error_app\n\n    def init_signals(self):\n        \"\"\"Initialize signal handlers for asyncio.\"\"\"\n        # Reset all signals first\n        for s in self.SIGNALS:\n            signal.signal(s, signal.SIG_DFL)\n\n        # Set up signal handlers via the event loop\n        self.loop.add_signal_handler(signal.SIGQUIT, self.handle_quit_signal)\n        self.loop.add_signal_handler(signal.SIGTERM, self.handle_exit_signal)\n        self.loop.add_signal_handler(signal.SIGINT, self.handle_quit_signal)\n        self.loop.add_signal_handler(signal.SIGUSR1, self.handle_usr1_signal)\n        self.loop.add_signal_handler(signal.SIGWINCH, self.handle_winch_signal)\n        self.loop.add_signal_handler(signal.SIGABRT, self.handle_abort_signal)\n\n    def handle_quit_signal(self):\n        \"\"\"Handle SIGQUIT/SIGINT - immediate shutdown.\"\"\"\n        self._quick_shutdown = True\n        if not self.alive:\n            # Already shutting down (SIGTERM was sent) - wake up the loop\n            return\n        self.alive = False\n        self.cfg.worker_int(self)\n\n    def handle_exit_signal(self):\n        \"\"\"Handle SIGTERM - graceful shutdown.\"\"\"\n        self.alive = False\n\n    def handle_usr1_signal(self):\n        \"\"\"Handle SIGUSR1 - reopen log files.\"\"\"\n        self.log.reopen_files()\n\n    def handle_winch_signal(self):\n        \"\"\"Handle SIGWINCH - ignored in worker.\"\"\"\n        self.log.debug(\"worker: SIGWINCH ignored.\")\n\n    def handle_abort_signal(self):\n        \"\"\"Handle SIGABRT - abort.\"\"\"\n        self.alive = False\n        self.cfg.worker_abort(self)\n        sys.exit(1)\n\n    def run(self):\n        \"\"\"Main entry point for the worker.\"\"\"\n        try:\n            self.loop.run_until_complete(self._serve())\n        except Exception as e:\n            self.log.exception(\"Worker exception: %s\", e)\n        finally:\n            self._cleanup()\n\n    async def _serve(self):\n        \"\"\"Main async serving loop.\"\"\"\n        # Run lifespan startup\n        lifespan_mode = getattr(self.cfg, 'asgi_lifespan', 'auto')\n        if lifespan_mode != \"off\":\n            from gunicorn.asgi.lifespan import LifespanManager\n            self.lifespan = LifespanManager(self.asgi, self.log, self.state)\n            try:\n                await self.lifespan.startup()\n            except Exception as e:\n                if lifespan_mode == \"on\":\n                    self.log.error(\"ASGI lifespan startup failed: %s\", e)\n                    return\n                else:\n                    # auto mode - app doesn't support lifespan\n                    self.log.debug(\"ASGI lifespan not supported by app: %s\", e)\n                    self.lifespan = None\n\n        # Create servers for each listener socket\n        ssl_context = self._get_ssl_context()\n\n        for sock in self.sockets:\n            try:\n                server = await self.loop.create_server(\n                    lambda: ASGIProtocol(self),\n                    sock=sock.sock,\n                    ssl=ssl_context,\n                    reuse_address=True,\n                    start_serving=True,\n                )\n                self.servers.append(server)\n                self.log.info(\"ASGI server listening on %s\", sock)\n            except Exception as e:\n                self.log.error(\"Failed to create server on %s: %s\", sock, e)\n\n        if not self.servers:\n            self.log.error(\"No servers could be started\")\n            return\n\n        # Main loop with heartbeat\n        try:\n            while self.alive:\n                self.notify()\n\n                # Check if parent is still alive\n                if self.ppid != os.getppid():\n                    self.log.info(\"Parent changed, shutting down: %s\", self)\n                    break\n\n                # Check connection limit\n                # (Connections are managed by nr_conns in ASGIProtocol)\n\n                await asyncio.sleep(1.0)\n\n        except asyncio.CancelledError:\n            pass\n\n        # Graceful shutdown\n        await self._shutdown()\n\n    async def _shutdown(self):\n        \"\"\"Perform graceful shutdown.\"\"\"\n        self.log.info(\"Worker shutting down...\")\n\n        # Stop accepting new connections\n        for server in self.servers:\n            server.close()\n\n        # Wait for servers to close (skip on quick shutdown)\n        if not self._quick_shutdown:\n            for server in self.servers:\n                if self._quick_shutdown:\n                    break\n                try:\n                    await asyncio.wait_for(server.wait_closed(), timeout=0.5)\n                except asyncio.TimeoutError:\n                    pass  # Check _quick_shutdown on next iteration\n\n        # Wait for in-flight connections (skip on quick shutdown)\n        if self.nr_conns > 0 and not self._quick_shutdown:\n            graceful_timeout = self.cfg.graceful_timeout\n            self.log.info(\"Waiting for %d connections to finish...\", self.nr_conns)\n            deadline = self.loop.time() + graceful_timeout\n            while self.nr_conns > 0 and self.loop.time() < deadline:\n                if self._quick_shutdown:\n                    self.log.info(\"Quick shutdown requested\")\n                    break\n                await asyncio.sleep(0.1)\n\n            if self.nr_conns > 0:\n                self.log.warning(\"Forcing close of %d connections\", self.nr_conns)\n\n        # Run lifespan shutdown (skip on quick shutdown)\n        if self.lifespan and not self._quick_shutdown:\n            try:\n                await self.lifespan.shutdown()\n            except Exception as e:\n                self.log.error(\"ASGI lifespan shutdown error: %s\", e)\n\n    def _get_ssl_context(self):\n        \"\"\"Get SSL context if configured.\"\"\"\n        if not self.cfg.is_ssl:\n            return None\n\n        try:\n            from gunicorn import sock\n            return sock.ssl_context(self.cfg)\n        except Exception as e:\n            self.log.error(\"Failed to create SSL context: %s\", e)\n            return None\n\n    def _cleanup(self):\n        \"\"\"Clean up resources on exit.\"\"\"\n        try:\n            # Cancel all pending tasks\n            pending = asyncio.all_tasks(self.loop)\n            for task in pending:\n                task.cancel()\n\n            # Run loop until all tasks are cancelled (with timeout on quick exit)\n            if pending:\n                gather = asyncio.gather(*pending, return_exceptions=True)\n                if self._quick_shutdown:\n                    # Quick exit - don't wait long for tasks to cancel\n                    try:\n                        self.loop.run_until_complete(\n                            asyncio.wait_for(gather, timeout=1.0)\n                        )\n                    except asyncio.TimeoutError:\n                        self.log.debug(\"Timeout waiting for tasks to cancel\")\n                else:\n                    self.loop.run_until_complete(gather)\n\n            self.loop.close()\n        except Exception as e:\n            self.log.debug(\"Cleanup error: %s\", e)\n\n        # Close sockets\n        for s in self.sockets:\n            try:\n                s.close()\n            except Exception:\n                pass\n"
  },
  {
    "path": "gunicorn/workers/geventlet.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n# DEPRECATION NOTICE: The eventlet worker is deprecated and will be removed\n# in Gunicorn 26.0. Eventlet itself is deprecated and no longer maintained.\n# Please migrate to gevent, gthread, or another supported worker type.\n# See: https://eventlet.readthedocs.io/en/latest/asyncio/migration.html\n\nimport warnings\n\nwarnings.warn(\n    \"The eventlet worker is deprecated and will be removed in Gunicorn 26.0. \"\n    \"Please migrate to gevent, gthread, or another supported worker type. \"\n    \"See: https://docs.gunicorn.org/en/stable/design.html#choosing-a-worker-type\",\n    DeprecationWarning,\n    stacklevel=2\n)\n\n# NOTE: eventlet import and monkey_patch() must happen before any other imports\n# to ensure all standard library modules are properly patched.\ntry:\n    import eventlet\nexcept ImportError:\n    raise RuntimeError(\"eventlet worker requires eventlet 0.40.3 or higher\")\nelse:\n    from packaging.version import parse as parse_version\n    if parse_version(eventlet.__version__) < parse_version('0.40.3'):\n        raise RuntimeError(\"eventlet worker requires eventlet 0.40.3 or higher\")\n\n# Perform monkey patching early, before importing other modules.\n# This ensures that all subsequent imports get the patched versions.\n# NOTE: hubs.use_hub() must NOT be called here - it creates OS resources\n# (like kqueue on macOS) that don't survive fork. It must be called in\n# each worker process after fork, in the patch() method.\neventlet.monkey_patch()\n\nfrom functools import partial  # noqa: E402\nimport sys  # noqa: E402\n\nfrom eventlet import hubs, greenthread  # noqa: E402\nfrom eventlet.greenio import GreenSocket  # noqa: E402\nimport eventlet.wsgi  # noqa: E402\nimport greenlet  # noqa: E402\n\nfrom gunicorn.workers.base_async import AsyncWorker  # noqa: E402\nfrom gunicorn.sock import ssl_wrap_socket  # noqa: E402\n\n# ALREADY_HANDLED is removed in 0.30.3+ now it's `WSGI_LOCAL.already_handled: bool`\n# https://github.com/eventlet/eventlet/pull/544\nEVENTLET_WSGI_LOCAL = getattr(eventlet.wsgi, \"WSGI_LOCAL\", None)\nEVENTLET_ALREADY_HANDLED = getattr(eventlet.wsgi, \"ALREADY_HANDLED\", None)\n\n\ndef _eventlet_socket_sendfile(self, file, offset=0, count=None):\n    # Based on the implementation in gevent which in turn is slightly\n    # modified from the standard library implementation.\n    if self.gettimeout() == 0:\n        raise ValueError(\"non-blocking sockets are not supported\")\n    if offset:\n        file.seek(offset)\n    blocksize = min(count, 8192) if count else 8192\n    total_sent = 0\n    # localize variable access to minimize overhead\n    file_read = file.read\n    sock_send = self.send\n    try:\n        while True:\n            if count:\n                blocksize = min(count - total_sent, blocksize)\n                if blocksize <= 0:\n                    break\n            data = memoryview(file_read(blocksize))\n            if not data:\n                break  # EOF\n            while True:\n                try:\n                    sent = sock_send(data)\n                except BlockingIOError:\n                    continue\n                else:\n                    total_sent += sent\n                    if sent < len(data):\n                        data = data[sent:]\n                    else:\n                        break\n        return total_sent\n    finally:\n        if total_sent > 0 and hasattr(file, 'seek'):\n            file.seek(offset + total_sent)\n\n\ndef _eventlet_serve(sock, handle, concurrency):\n    \"\"\"\n    Serve requests forever.\n\n    This code is nearly identical to ``eventlet.convenience.serve`` except\n    that it attempts to join the pool at the end, which allows for gunicorn\n    graceful shutdowns.\n    \"\"\"\n    pool = eventlet.greenpool.GreenPool(concurrency)\n    server_gt = eventlet.greenthread.getcurrent()\n\n    while True:\n        try:\n            conn, addr = sock.accept()\n            gt = pool.spawn(handle, conn, addr)\n            gt.link(_eventlet_stop, server_gt, conn)\n            conn, addr, gt = None, None, None\n        except eventlet.StopServe:\n            sock.close()\n            pool.waitall()\n            return\n\n\ndef _eventlet_stop(client, server, conn):\n    \"\"\"\n    Stop a greenlet handling a request and close its connection.\n\n    This code is lifted from eventlet so as not to depend on undocumented\n    functions in the library.\n    \"\"\"\n    try:\n        try:\n            client.wait()\n        finally:\n            conn.close()\n    except greenlet.GreenletExit:\n        pass\n    except Exception:\n        greenthread.kill(server, *sys.exc_info())\n\n\ndef patch_sendfile():\n    # As of eventlet 0.25.1, GreenSocket.sendfile doesn't exist,\n    # meaning the native implementations of socket.sendfile will be used.\n    # If os.sendfile exists, it will attempt to use that, failing explicitly\n    # if the socket is in non-blocking mode, which the underlying\n    # socket object /is/. Even the regular _sendfile_use_send will\n    # fail in that way; plus, it would use the underlying socket.send which isn't\n    # properly cooperative. So we have to monkey-patch a working socket.sendfile()\n    # into GreenSocket; in this method, `self.send` will be the GreenSocket's\n    # send method which is properly cooperative.\n    if not hasattr(GreenSocket, 'sendfile'):\n        GreenSocket.sendfile = _eventlet_socket_sendfile\n\n\nclass EventletWorker(AsyncWorker):\n\n    def patch(self):\n        # NOTE: eventlet.monkey_patch() is called at module import time to\n        # ensure all imports are properly patched. However, hubs.use_hub()\n        # must be called here (after fork) because it creates OS resources\n        # like kqueue that don't survive fork.\n        hubs.use_hub()\n        patch_sendfile()\n\n    def is_already_handled(self, respiter):\n        # eventlet >= 0.30.3\n        if getattr(EVENTLET_WSGI_LOCAL, \"already_handled\", None):\n            raise StopIteration()\n        # eventlet < 0.30.3\n        if respiter == EVENTLET_ALREADY_HANDLED:\n            raise StopIteration()\n        return super().is_already_handled(respiter)\n\n    def init_process(self):\n        self.log.warning(\n            \"The eventlet worker is DEPRECATED and will be removed in Gunicorn 26.0. \"\n            \"Please migrate to gevent, gthread, or another supported worker type.\"\n        )\n        self.patch()\n        super().init_process()\n\n    def handle_quit(self, sig, frame):\n        eventlet.spawn(super().handle_quit, sig, frame)\n\n    def handle_usr1(self, sig, frame):\n        eventlet.spawn(super().handle_usr1, sig, frame)\n\n    def timeout_ctx(self):\n        return eventlet.Timeout(self.cfg.keepalive or None, False)\n\n    def handle(self, listener, client, addr):\n        if self.cfg.is_ssl:\n            client = ssl_wrap_socket(client, self.cfg)\n        super().handle(listener, client, addr)\n\n    def run(self):\n        acceptors = []\n        for sock in self.sockets:\n            gsock = GreenSocket(sock)\n            gsock.setblocking(1)\n            hfun = partial(self.handle, gsock)\n            acceptor = eventlet.spawn(_eventlet_serve, gsock, hfun,\n                                      self.worker_connections)\n\n            acceptors.append(acceptor)\n            eventlet.sleep(0.0)\n\n        while self.alive:\n            self.notify()\n            eventlet.sleep(1.0)\n\n        self.notify()\n        t = None\n        try:\n            with eventlet.Timeout(self.cfg.graceful_timeout) as t:\n                for a in acceptors:\n                    a.kill(eventlet.StopServe())\n                for a in acceptors:\n                    a.wait()\n        except eventlet.Timeout as te:\n            if te != t:\n                raise\n            for a in acceptors:\n                a.kill()\n"
  },
  {
    "path": "gunicorn/workers/ggevent.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport os\nimport sys\nfrom datetime import datetime\nfrom functools import partial\nimport time\n\ntry:\n    import gevent\nexcept ImportError:\n    raise RuntimeError(\"gevent worker requires gevent 24.10.1 or higher\")\nelse:\n    from packaging.version import parse as parse_version\n    if parse_version(gevent.__version__) < parse_version('24.10.1'):\n        raise RuntimeError(\"gevent worker requires gevent 24.10.1 or higher\")\n\nfrom gevent.pool import Pool\nfrom gevent.server import StreamServer\nfrom gevent import hub, monkey, socket, pywsgi\n\nimport gunicorn\nfrom gunicorn.http.wsgi import base_environ\nfrom gunicorn.sock import ssl_context\nfrom gunicorn.workers.base_async import AsyncWorker\n\nVERSION = \"gevent/%s gunicorn/%s\" % (gevent.__version__, gunicorn.__version__)\n\n\nclass GeventWorker(AsyncWorker):\n\n    server_class = None\n    wsgi_handler = None\n\n    def patch(self):\n        monkey.patch_all()\n\n        # patch sockets\n        sockets = []\n        for s in self.sockets:\n            sockets.append(socket.socket(s.FAMILY, socket.SOCK_STREAM,\n                                         fileno=s.sock.detach()))\n        self.sockets = sockets\n\n    def notify(self):\n        super().notify()\n        if self.ppid != os.getppid():\n            self.log.info(\"Parent changed, shutting down: %s\", self)\n            sys.exit(0)\n\n    def timeout_ctx(self):\n        return gevent.Timeout(self.cfg.keepalive, False)\n\n    def run(self):\n        servers = []\n        ssl_args = {}\n\n        if self.cfg.is_ssl:\n            ssl_args = {\"ssl_context\": ssl_context(self.cfg)}\n\n        for s in self.sockets:\n            s.setblocking(1)\n            pool = Pool(self.worker_connections)\n            if self.server_class is not None:\n                environ = base_environ(self.cfg)\n                environ.update({\n                    \"wsgi.multithread\": True,\n                    \"SERVER_SOFTWARE\": VERSION,\n                })\n                server = self.server_class(\n                    s, application=self.wsgi, spawn=pool, log=self.log,\n                    handler_class=self.wsgi_handler, environ=environ,\n                    **ssl_args)\n            else:\n                hfun = partial(self.handle, s)\n                server = StreamServer(s, handle=hfun, spawn=pool, **ssl_args)\n                if self.cfg.workers > 1:\n                    server.max_accept = 1\n\n            server.start()\n            servers.append(server)\n\n        while self.alive:\n            self.notify()\n            gevent.sleep(1.0)\n\n        try:\n            # Stop accepting requests\n            for server in servers:\n                server.close()\n\n            # Handle current requests until graceful_timeout\n            ts = time.time()\n            while time.time() - ts <= self.cfg.graceful_timeout:\n                accepting = 0\n                for server in servers:\n                    if server.pool.free_count() != server.pool.size:\n                        accepting += 1\n\n                # if no server is accepting a connection, we can exit\n                if not accepting:\n                    return\n\n                self.notify()\n                gevent.sleep(1.0)\n\n            # Force kill all the active handlers\n            self.log.warning(\"Worker graceful timeout (pid:%s)\", self.pid)\n            for server in servers:\n                server.stop(timeout=1)\n        except Exception:\n            pass\n\n    def handle(self, listener, client, addr):\n        # Connected socket timeout defaults to socket.getdefaulttimeout().\n        # This forces to blocking mode.\n        client.setblocking(1)\n        super().handle(listener, client, addr)\n\n    def handle_request(self, listener_name, req, sock, addr):\n        try:\n            super().handle_request(listener_name, req, sock, addr)\n        except gevent.GreenletExit:\n            pass\n        except SystemExit:\n            pass\n\n    def handle_quit(self, sig, frame):\n        # Move this out of the signal handler so we can use\n        # blocking calls. See #1126\n        gevent.spawn(super().handle_quit, sig, frame)\n\n    def handle_usr1(self, sig, frame):\n        # Make the gevent workers handle the usr1 signal\n        # by deferring to a new greenlet. See #1645\n        gevent.spawn(super().handle_usr1, sig, frame)\n\n    def init_process(self):\n        self.patch()\n        hub.reinit()\n        super().init_process()\n\n\nclass GeventResponse:\n\n    status = None\n    headers = None\n    sent = None\n\n    def __init__(self, status, headers, clength):\n        self.status = status\n        self.headers = headers\n        self.sent = clength\n\n\nclass PyWSGIHandler(pywsgi.WSGIHandler):\n\n    def log_request(self):\n        start = datetime.fromtimestamp(self.time_start)\n        finish = datetime.fromtimestamp(self.time_finish)\n        response_time = finish - start\n        resp_headers = getattr(self, 'response_headers', {})\n\n        # Status is expected to be a string but is encoded to bytes in gevent for PY3\n        # Except when it isn't because gevent uses hardcoded strings for network errors.\n        status = self.status.decode() if isinstance(self.status, bytes) else self.status\n        resp = GeventResponse(status, resp_headers, self.response_length)\n        if hasattr(self, 'headers'):\n            req_headers = self.headers.items()\n        else:\n            req_headers = []\n        self.server.log.access(resp, req_headers, self.environ, response_time)\n\n    def get_environ(self):\n        env = super().get_environ()\n        env['gunicorn.sock'] = self.socket\n        env['RAW_URI'] = self.path\n        return env\n\n\nclass PyWSGIServer(pywsgi.WSGIServer):\n    pass\n\n\nclass GeventPyWSGIWorker(GeventWorker):\n    \"The Gevent StreamServer based workers.\"\n    server_class = PyWSGIServer\n    wsgi_handler = PyWSGIHandler\n"
  },
  {
    "path": "gunicorn/workers/gthread.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n# design:\n# A threaded worker accepts connections in the main loop, accepted\n# connections are added to the thread pool as a connection job.\n# Keepalive connections are put back in the loop waiting for an event.\n# If no event happen after the keep alive timeout, the connection is\n# closed.\n# pylint: disable=no-else-break\n\nfrom concurrent import futures\nimport errno\nimport os\nimport queue\nimport selectors\nimport socket\nimport ssl\nimport sys\nimport time\nfrom collections import deque\nfrom datetime import datetime\nfrom functools import partial\n\nfrom . import base\nfrom .. import http\nfrom .. import util\nfrom .. import sock\nfrom ..http import wsgi\n\n\n# Sentinel value to indicate connection should be deferred back to poller\n_DEFER = object()\n\n# Default timeout (in seconds) for waiting for request data in worker thread.\n# If no data arrives within this timeout, the connection is deferred back to\n# the main poller to prevent thread pool exhaustion from slow clients.\nDEFAULT_WORKER_DATA_TIMEOUT = 5.0\n\n\nclass TConn:\n\n    def __init__(self, cfg, sock, client, server):\n        self.cfg = cfg\n        self.sock = sock\n        self.client = client\n        self.server = server\n\n        self.timeout = None\n        self.parser = None\n        self.initialized = False\n        self.is_http2 = False\n        # Track if we've already waited for data (to avoid waiting again after defer)\n        self.data_ready = False\n\n        # set the socket to non blocking\n        self.sock.setblocking(False)\n\n    def init(self):\n        # Guard against double initialization\n        if self.initialized:\n            return\n        self.initialized = True\n        self.sock.setblocking(True)\n\n        if self.parser is None:\n            # wrap the socket if needed\n            if self.cfg.is_ssl:\n                self.sock = sock.ssl_wrap_socket(self.sock, self.cfg)\n\n                # Complete the handshake to ensure ALPN negotiation is done\n                # (needed if do_handshake_on_connect is False)\n                if not self.cfg.do_handshake_on_connect:\n                    self.sock.do_handshake()\n\n                # Check if HTTP/2 was negotiated via ALPN\n                if sock.is_http2_negotiated(self.sock):\n                    self.is_http2 = True\n                    self.parser = http.get_parser(\n                        self.cfg, self.sock, self.client, http2_connection=True\n                    )\n                    self.parser.initiate_connection()\n                    return\n\n            # initialize the HTTP/1.x parser\n            self.parser = http.get_parser(self.cfg, self.sock, self.client)\n\n    def set_timeout(self):\n        # Use monotonic clock for reliability (time.time() can jump due to NTP)\n        self.timeout = time.monotonic() + self.cfg.keepalive\n\n    def wait_for_data(self, timeout):\n        \"\"\"Wait for data to be available on the socket.\n\n        Uses selectors to wait for the socket to become readable within\n        the given timeout. This prevents slow clients from blocking\n        thread pool slots indefinitely.\n\n        Args:\n            timeout: Maximum time to wait in seconds.\n\n        Returns:\n            True if data is available, False if timeout expired.\n        \"\"\"\n        if self.data_ready:\n            return True\n\n        # Use a temporary selector to wait for data\n        sel = selectors.DefaultSelector()\n        try:\n            sel.register(self.sock, selectors.EVENT_READ)\n            events = sel.select(timeout=timeout)\n            if events:\n                self.data_ready = True\n                return True\n            return False\n        except (OSError, ValueError):\n            # Socket closed or invalid\n            return False\n        finally:\n            sel.close()\n\n    def close(self):\n        util.close(self.sock)\n\n\nclass PollableMethodQueue:\n    \"\"\"Thread-safe queue that can wake up a selector.\n\n    Uses a pipe to allow worker threads to signal the main thread\n    when work is ready, enabling lock-free coordination.\n\n    This approach is compatible with all POSIX systems including\n    Linux, macOS, FreeBSD, OpenBSD, and NetBSD. The pipe is set to\n    non-blocking mode to prevent worker threads from blocking if\n    the pipe buffer fills up under extreme load.\n    \"\"\"\n\n    def __init__(self):\n        self._read_fd = None\n        self._write_fd = None\n        self._queue = None\n\n    def init(self):\n        \"\"\"Initialize the pipe and queue.\"\"\"\n        self._read_fd, self._write_fd = os.pipe()\n        # Set both ends to non-blocking:\n        # - Write: prevents worker threads from blocking if buffer is full\n        # - Read: allows run_callbacks to drain without blocking\n        os.set_blocking(self._read_fd, False)\n        os.set_blocking(self._write_fd, False)\n        self._queue = queue.SimpleQueue()\n\n    def close(self):\n        \"\"\"Close the pipe file descriptors.\"\"\"\n        if self._read_fd is not None:\n            try:\n                os.close(self._read_fd)\n            except OSError:\n                pass\n        if self._write_fd is not None:\n            try:\n                os.close(self._write_fd)\n            except OSError:\n                pass\n\n    def fileno(self):\n        \"\"\"Return the readable file descriptor for selector registration.\"\"\"\n        return self._read_fd\n\n    def defer(self, callback, *args):\n        \"\"\"Queue a callback to be run on the main thread.\n\n        The callback is added to the queue first, then a wake-up byte\n        is written to the pipe. If the pipe write fails (buffer full),\n        it's safe to ignore because the main thread will eventually\n        drain the queue when it reads other wake-up bytes.\n        \"\"\"\n        self._queue.put(partial(callback, *args))\n        try:\n            os.write(self._write_fd, b'\\x00')\n        except OSError:\n            # Pipe buffer full (EAGAIN/EWOULDBLOCK) - safe to ignore\n            # The main thread will still process the queue\n            pass\n\n    def run_callbacks(self, _fileobj, max_callbacks=50):\n        \"\"\"Run queued callbacks. Called when the pipe is readable.\n\n        Drains all available wake-up bytes and runs corresponding callbacks.\n        The max_callbacks limit prevents starvation of other event sources.\n        \"\"\"\n        # Read all available wake-up bytes (up to limit)\n        try:\n            data = os.read(self._read_fd, max_callbacks)\n        except OSError:\n            return\n\n        # Run callbacks for each byte read, plus any extras in queue\n        # (extras can accumulate if pipe writes were dropped)\n        callbacks_run = 0\n        while callbacks_run < len(data) + 10:  # +10 to drain dropped writes\n            try:\n                callback = self._queue.get_nowait()\n                callback()\n                callbacks_run += 1\n            except queue.Empty:\n                break\n\n\nclass ThreadWorker(base.Worker):\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.worker_connections = self.cfg.worker_connections\n        self.max_keepalived = self.cfg.worker_connections - self.cfg.threads\n\n        self.tpool = None\n        self.poller = None\n        self.method_queue = PollableMethodQueue()\n        self.keepalived_conns = deque()\n        # Connections waiting for data (deferred from thread pool)\n        self.pending_conns = deque()\n        self.nr_conns = 0\n        self._accepting = False\n\n    @classmethod\n    def check_config(cls, cfg, log):\n        max_keepalived = cfg.worker_connections - cfg.threads\n\n        if max_keepalived <= 0 and cfg.keepalive:\n            log.warning(\"No keepalived connections can be handled. \" +\n                        \"Check the number of worker connections and threads.\")\n\n    def init_process(self):\n        self.tpool = self.get_thread_pool()\n        self.poller = selectors.DefaultSelector()\n        self.method_queue.init()\n        super().init_process()\n\n    def get_thread_pool(self):\n        \"\"\"Override this method to customize how the thread pool is created\"\"\"\n        return futures.ThreadPoolExecutor(max_workers=self.cfg.threads)\n\n    def handle_exit(self, sig, frame):\n        \"\"\"Handle SIGTERM - begin graceful shutdown.\"\"\"\n        if self.alive:\n            self.alive = False\n            # Wake up the poller so it can start shutdown\n            self.method_queue.defer(lambda: None)\n\n    def handle_quit(self, sig, frame):\n        \"\"\"Handle SIGQUIT - immediate shutdown.\"\"\"\n        self.tpool.shutdown(wait=False)\n        super().handle_quit(sig, frame)\n\n    def set_accept_enabled(self, enabled):\n        \"\"\"Enable or disable accepting new connections.\"\"\"\n        if enabled == self._accepting:\n            return\n\n        for listener in self.sockets:\n            if enabled:\n                listener.setblocking(False)\n                self.poller.register(listener, selectors.EVENT_READ, self.accept)\n            else:\n                self.poller.unregister(listener)\n\n        self._accepting = enabled\n\n    def enqueue_req(self, conn):\n        \"\"\"Submit connection to thread pool for processing.\"\"\"\n        fs = self.tpool.submit(self.handle, conn)\n        fs.add_done_callback(\n            lambda fut: self.method_queue.defer(self.finish_request, conn, fut))\n\n    def accept(self, listener):\n        \"\"\"Accept a new connection from a listener socket.\"\"\"\n        try:\n            client_sock, client_addr = listener.accept()\n            self.nr_conns += 1\n            client_sock.setblocking(True)\n\n            conn = TConn(self.cfg, client_sock, client_addr, listener.getsockname())\n\n            # Submit directly to thread pool for processing\n            self.enqueue_req(conn)\n        except OSError as e:\n            if e.errno not in (errno.EAGAIN, errno.ECONNABORTED, errno.EWOULDBLOCK):\n                raise\n\n    def on_client_socket_readable(self, conn, client):\n        \"\"\"Handle a keepalive connection becoming readable.\"\"\"\n        self.poller.unregister(client)\n        self.keepalived_conns.remove(conn)\n\n        # Submit to thread pool for processing\n        self.enqueue_req(conn)\n\n    def on_pending_socket_readable(self, conn, client):\n        \"\"\"Handle a pending (deferred) connection becoming readable.\"\"\"\n        self.poller.unregister(client)\n        self.pending_conns.remove(conn)\n\n        # Mark data as ready so we don't wait again in handle()\n        conn.data_ready = True\n\n        # Submit to thread pool for processing\n        self.enqueue_req(conn)\n\n    def murder_keepalived(self):\n        \"\"\"Close expired keepalive connections.\"\"\"\n        now = time.monotonic()\n        while self.keepalived_conns:\n            conn = self.keepalived_conns[0]\n            delta = conn.timeout - now\n            if delta > 0:\n                break\n\n            # Connection has timed out\n            self.keepalived_conns.popleft()\n            try:\n                self.poller.unregister(conn.sock)\n            except (OSError, KeyError, ValueError):\n                pass  # Already unregistered\n            self.nr_conns -= 1\n            conn.close()\n\n    def murder_pending(self):\n        \"\"\"Close expired pending connections (waiting for initial data).\"\"\"\n        now = time.monotonic()\n        while self.pending_conns:\n            conn = self.pending_conns[0]\n            delta = conn.timeout - now\n            if delta > 0:\n                break\n\n            # Connection has timed out waiting for data\n            self.pending_conns.popleft()\n            try:\n                self.poller.unregister(conn.sock)\n            except (OSError, KeyError, ValueError):\n                pass  # Already unregistered\n            self.nr_conns -= 1\n            conn.close()\n\n    def is_parent_alive(self):\n        # If our parent changed then we shut down.\n        if self.ppid != os.getppid():\n            self.log.info(\"Parent changed, shutting down: %s\", self)\n            return False\n        return True\n\n    def wait_for_and_dispatch_events(self, timeout):\n        \"\"\"Wait for events and dispatch callbacks.\"\"\"\n        try:\n            events = self.poller.select(timeout)\n            for key, _ in events:\n                callback = key.data\n                callback(key.fileobj)\n        except OSError as e:\n            if e.errno != errno.EINTR:\n                raise\n\n    def run(self):\n        # Register the method queue with the poller\n        self.poller.register(self.method_queue.fileno(),\n                             selectors.EVENT_READ,\n                             self.method_queue.run_callbacks)\n\n        # Start accepting connections\n        self.set_accept_enabled(True)\n\n        while self.alive:\n            # Notify the arbiter we are alive\n            self.notify()\n\n            # Check if we can accept more connections\n            can_accept = self.nr_conns < self.worker_connections\n            if can_accept != self._accepting:\n                self.set_accept_enabled(can_accept)\n\n            # Wait for events (unified event loop - no futures.wait())\n            self.wait_for_and_dispatch_events(timeout=1.0)\n\n            if not self.is_parent_alive():\n                break\n\n            # Handle keepalive and pending connection timeouts\n            self.murder_keepalived()\n            self.murder_pending()\n\n        # Graceful shutdown: stop accepting but handle existing connections\n        self.set_accept_enabled(False)\n\n        # Wait for in-flight connections within grace period\n        graceful_timeout = time.monotonic() + self.cfg.graceful_timeout\n        while self.nr_conns > 0:\n            time_remaining = max(graceful_timeout - time.monotonic(), 0)\n            if time_remaining == 0:\n                break\n            self.wait_for_and_dispatch_events(timeout=time_remaining)\n            self.murder_keepalived()\n            self.murder_pending()\n\n        # Cleanup\n        self.tpool.shutdown(wait=False)\n        self.poller.close()\n        self.method_queue.close()\n\n        for s in self.sockets:\n            s.close()\n\n    def finish_request(self, conn, fs):\n        \"\"\"Handle completion of a request (called via method_queue on main thread).\"\"\"\n        try:\n            result = fs.result() if not fs.cancelled() else False\n\n            if result is _DEFER and self.alive:\n                # Connection deferred - no data arrived within timeout.\n                # Put it on the poller to wait for data without consuming a thread.\n                conn.sock.setblocking(False)\n                # Use keepalive timeout for pending connections too\n                conn.timeout = time.monotonic() + self.cfg.keepalive\n                self.pending_conns.append(conn)\n                self.poller.register(conn.sock, selectors.EVENT_READ,\n                                     partial(self.on_pending_socket_readable, conn))\n            elif result and self.alive:\n                # Keepalive - put connection back in the poller\n                conn.sock.setblocking(False)\n                conn.set_timeout()\n                self.keepalived_conns.append(conn)\n                self.poller.register(conn.sock, selectors.EVENT_READ,\n                                     partial(self.on_client_socket_readable, conn))\n            else:\n                self.nr_conns -= 1\n                conn.close()\n        except Exception:\n            self.nr_conns -= 1\n            conn.close()\n\n    def handle(self, conn):\n        \"\"\"Handle a request on a connection. Runs in a worker thread.\"\"\"\n        req = None\n        try:\n            # For new connections (not yet initialized), wait for data with timeout\n            # to prevent slow clients from blocking thread pool slots indefinitely.\n            # Skip this for already-initialized connections (keepalive, deferred)\n            # since they're coming from the poller and data is already available.\n            if not conn.initialized and not conn.data_ready:\n                # Wait for data with timeout before committing this thread\n                if not conn.wait_for_data(DEFAULT_WORKER_DATA_TIMEOUT):\n                    # No data within timeout - defer to poller\n                    return _DEFER\n\n            # Always ensure blocking mode in worker thread.\n            # Critical for keepalive connections: the socket is set to non-blocking\n            # for the selector in finish_request(), but must be blocking for\n            # request/body reading to avoid SSLWantReadError on SSL connections.\n            conn.sock.setblocking(True)\n\n            # Initialize connection in worker thread to handle SSL errors gracefully\n            # (ENOTCONN from ssl_wrap_socket would crash main thread otherwise)\n            conn.init()\n\n            # HTTP/2 connections require special handling\n            if conn.is_http2:\n                return self.handle_http2(conn)\n\n            req = next(conn.parser)\n            if not req:\n                return False\n\n            # Handle the request\n            keepalive = self.handle_request(req, conn)\n            if keepalive:\n                # Discard any unread request body before keepalive\n                # to prevent socket appearing readable due to leftover bytes\n                conn.parser.finish_body()\n                return True\n        except http.errors.NoMoreData as e:\n            self.log.debug(\"Ignored premature client disconnection. %s\", e)\n        except StopIteration as e:\n            self.log.debug(\"Closing connection. %s\", e)\n        except ssl.SSLError as e:\n            if e.args[0] == ssl.SSL_ERROR_EOF:\n                self.log.debug(\"ssl connection closed\")\n                conn.sock.close()\n            else:\n                self.log.debug(\"Error processing SSL request.\")\n                self.handle_error(req, conn.sock, conn.client, e)\n        except OSError as e:\n            if e.errno not in (errno.EPIPE, errno.ECONNRESET, errno.ENOTCONN):\n                self.log.exception(\"Socket error processing request.\")\n            else:\n                if e.errno == errno.ECONNRESET:\n                    self.log.debug(\"Ignoring connection reset\")\n                elif e.errno == errno.ENOTCONN:\n                    self.log.debug(\"Ignoring socket not connected\")\n                else:\n                    self.log.debug(\"Ignoring connection epipe\")\n        except Exception as e:\n            self.handle_error(req, conn.sock, conn.client, e)\n\n        return False\n\n    def handle_http2(self, conn):\n        \"\"\"Handle an HTTP/2 connection. Runs in a worker thread.\n\n        HTTP/2 connections are persistent and multiplex multiple streams.\n        We handle all streams until the connection is closed.\n\n        Returns:\n            False (HTTP/2 connections don't use keepalive polling)\n        \"\"\"\n        h2_conn = conn.parser  # HTTP2ServerConnection\n\n        try:\n            while not h2_conn.is_closed and self.alive:\n                # Receive data and get completed requests\n                requests = h2_conn.receive_data()\n\n                for req in requests:\n                    try:\n                        self.handle_http2_request(req, conn, h2_conn)\n                    except Exception as e:\n                        self.log.exception(\"Error handling HTTP/2 request\")\n                        try:\n                            h2_conn.send_error(req.stream.stream_id, 500, str(e))\n                        except Exception:\n                            pass\n                    finally:\n                        # Cleanup stream after processing\n                        h2_conn.cleanup_stream(req.stream.stream_id)\n\n                # Check if we need to close\n                if not self.alive:\n                    h2_conn.close()\n                    break\n\n        except http.errors.NoMoreData:\n            self.log.debug(\"HTTP/2 connection closed by client\")\n        except ssl.SSLError as e:\n            if e.args[0] == ssl.SSL_ERROR_EOF:\n                self.log.debug(\"HTTP/2 SSL connection closed\")\n            else:\n                self.log.debug(\"HTTP/2 SSL error: %s\", e)\n        except OSError as e:\n            if e.errno not in (errno.EPIPE, errno.ECONNRESET, errno.ENOTCONN):\n                self.log.exception(\"HTTP/2 socket error\")\n        except Exception:\n            self.log.exception(\"HTTP/2 connection error\")\n\n        return False\n\n    def handle_http2_request(self, req, conn, h2_conn):\n        \"\"\"Handle a single HTTP/2 request/stream.\"\"\"\n        environ = {}\n        resp = None\n        stream_id = req.stream.stream_id\n\n        try:\n            self.cfg.pre_request(self, req)\n            request_start = datetime.now()\n\n            # Create WSGI environ\n            resp, environ = wsgi.create(req, conn.sock, conn.client,\n                                        conn.server, self.cfg)\n            environ[\"wsgi.multithread\"] = True\n            environ[\"HTTP_VERSION\"] = \"2\"  # Indicate HTTP/2\n\n            # Replace wsgi.early_hints with HTTP/2-specific version\n            def send_early_hints_h2(headers):\n                \"\"\"Send 103 Early Hints over HTTP/2.\"\"\"\n                h2_conn.send_informational(stream_id, 103, headers)\n\n            environ[\"wsgi.early_hints\"] = send_early_hints_h2\n\n            # Add HTTP/2 trailer support\n            pending_trailers = []\n\n            def send_trailers_h2(trailers):\n                \"\"\"Queue trailers to be sent after response body.\"\"\"\n                pending_trailers.extend(trailers)\n\n            environ[\"gunicorn.http2.send_trailers\"] = send_trailers_h2\n\n            self.nr += 1\n            if self.nr >= self.max_requests:\n                if self.alive:\n                    self.log.info(\"Autorestarting worker after current request.\")\n                    self.alive = False\n\n            # Run WSGI app\n            respiter = self.wsgi(environ, resp.start_response)\n\n            # Collect response body\n            response_body = b''\n            try:\n                if hasattr(respiter, '__iter__'):\n                    for item in respiter:\n                        if item:\n                            response_body += item\n            finally:\n                if hasattr(respiter, \"close\"):\n                    respiter.close()\n\n            # Send response via HTTP/2\n            if pending_trailers:\n                # Send headers, body, then trailers separately\n                # Build response headers with :status pseudo-header\n                response_headers = [(':status', str(resp.status_code))]\n                for name, value in resp.headers:\n                    response_headers.append((name.lower(), str(value)))\n\n                # Send headers without ending stream\n                h2_conn.h2_conn.send_headers(stream_id, response_headers, end_stream=False)\n                stream = h2_conn.streams[stream_id]\n                stream.send_headers(response_headers, end_stream=False)\n                h2_conn._send_pending_data()\n\n                # Send body without ending stream\n                if response_body:\n                    h2_conn.h2_conn.send_data(stream_id, response_body, end_stream=False)\n                    stream.send_data(response_body, end_stream=False)\n                    h2_conn._send_pending_data()\n\n                # Send trailers (ends stream)\n                h2_conn.send_trailers(stream_id, pending_trailers)\n            else:\n                # No trailers, use standard response\n                h2_conn.send_response(\n                    stream_id,\n                    resp.status_code,\n                    resp.headers,\n                    response_body\n                )\n\n            request_time = datetime.now() - request_start\n            self.log.access(resp, req, environ, request_time)\n\n        finally:\n            try:\n                self.cfg.post_request(self, req, environ, resp)\n            except Exception:\n                self.log.exception(\"Exception in post_request hook\")\n\n    def handle_request(self, req, conn):\n        environ = {}\n        resp = None\n        try:\n            self.cfg.pre_request(self, req)\n            request_start = datetime.now()\n            resp, environ = wsgi.create(req, conn.sock, conn.client,\n                                        conn.server, self.cfg)\n            environ[\"wsgi.multithread\"] = True\n            self.nr += 1\n            if self.nr >= self.max_requests:\n                if self.alive:\n                    self.log.info(\"Autorestarting worker after current request.\")\n                    self.alive = False\n                resp.force_close()\n\n            if not self.alive or not self.cfg.keepalive:\n                resp.force_close()\n            elif len(self.keepalived_conns) >= self.max_keepalived:\n                resp.force_close()\n\n            respiter = self.wsgi(environ, resp.start_response)\n            try:\n                if isinstance(respiter, environ['wsgi.file_wrapper']):\n                    resp.write_file(respiter)\n                else:\n                    for item in respiter:\n                        resp.write(item)\n\n                resp.close()\n            finally:\n                request_time = datetime.now() - request_start\n                self.log.access(resp, req, environ, request_time)\n                if hasattr(respiter, \"close\"):\n                    respiter.close()\n\n            if resp.should_close():\n                self.log.debug(\"Closing connection.\")\n                return False\n        except OSError:\n            # pass to next try-except level\n            util.reraise(*sys.exc_info())\n        except Exception:\n            if resp and resp.headers_sent:\n                # If the requests have already been sent, we should close the\n                # connection to indicate the error.\n                self.log.exception(\"Error handling request\")\n                try:\n                    conn.sock.shutdown(socket.SHUT_RDWR)\n                    conn.sock.close()\n                except OSError:\n                    pass\n                raise StopIteration()\n            raise\n        finally:\n            try:\n                self.cfg.post_request(self, req, environ, resp)\n            except Exception:\n                self.log.exception(\"Exception in post_request hook\")\n\n        return True\n"
  },
  {
    "path": "gunicorn/workers/gtornado.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport os\nimport sys\n\ntry:\n    import tornado\nexcept ImportError:\n    raise RuntimeError(\"You need tornado installed to use this worker.\")\nimport tornado.web\nimport tornado.httpserver\nfrom tornado.ioloop import IOLoop, PeriodicCallback\nfrom tornado.wsgi import WSGIContainer\nfrom gunicorn.workers.base import Worker\nfrom gunicorn import __version__ as gversion\nfrom gunicorn.sock import ssl_context\n\n\nclass TornadoWorker(Worker):\n\n    @classmethod\n    def setup(cls):\n        web = sys.modules.pop(\"tornado.web\")\n        old_clear = web.RequestHandler.clear\n\n        def clear(self):\n            old_clear(self)\n            if \"Gunicorn\" not in self._headers[\"Server\"]:\n                self._headers[\"Server\"] += \" (Gunicorn/%s)\" % gversion\n        web.RequestHandler.clear = clear\n        sys.modules[\"tornado.web\"] = web\n\n    def handle_exit(self, sig, frame):\n        if self.alive:\n            super().handle_exit(sig, frame)\n\n    def handle_request(self):\n        self.nr += 1\n        if self.alive and self.nr >= self.max_requests:\n            self.log.info(\"Autorestarting worker after current request.\")\n            self.alive = False\n\n    def watchdog(self):\n        if self.alive:\n            self.notify()\n\n        if self.ppid != os.getppid():\n            self.log.info(\"Parent changed, shutting down: %s\", self)\n            self.alive = False\n\n    def heartbeat(self):\n        if not self.alive:\n            if self.server_alive:\n                if hasattr(self, 'server'):\n                    try:\n                        self.server.stop()\n                    except Exception:\n                        pass\n                self.server_alive = False\n            else:\n                for callback in self.callbacks:\n                    callback.stop()\n                self.ioloop.stop()\n\n    def init_process(self):\n        # IOLoop cannot survive a fork or be shared across processes\n        # in any way. When multiple processes are being used, each process\n        # should create its own IOLoop. We should clear current IOLoop\n        # if exists before os.fork.\n        IOLoop.clear_current()\n        super().init_process()\n\n    def run(self):\n        self.ioloop = IOLoop.instance()\n        self.alive = True\n        self.server_alive = False\n\n        # Warn if HTTP/2 is requested - tornado worker doesn't support it\n        if 'h2' in self.cfg.http_protocols:\n            self.log.warning(\n                \"HTTP/2 is not supported by the tornado worker. \"\n                \"Use gthread, gevent, eventlet, or asgi workers for HTTP/2 support. \"\n                \"Falling back to HTTP/1.1 only.\"\n            )\n\n        self.callbacks = []\n        self.callbacks.append(PeriodicCallback(self.watchdog, 1000))\n        self.callbacks.append(PeriodicCallback(self.heartbeat, 1000))\n        for callback in self.callbacks:\n            callback.start()\n\n        # Assume the app is a WSGI callable if its not an\n        # instance of tornado.web.Application or WSGIContainer\n        app = self.wsgi\n        if not isinstance(app, WSGIContainer) and \\\n                not isinstance(app, tornado.web.Application):\n            app = WSGIContainer(app)\n\n        worker = self\n\n        class _HTTPServer(tornado.httpserver.HTTPServer):\n\n            def on_close(self, server_conn):\n                worker.handle_request()\n                super().on_close(server_conn)\n\n        if self.cfg.is_ssl:\n            server = _HTTPServer(app, ssl_options=ssl_context(self.cfg))\n        else:\n            server = _HTTPServer(app)\n\n        self.server = server\n        self.server_alive = True\n\n        for s in self.sockets:\n            s.setblocking(0)\n            server.add_socket(s)\n\n        server.no_keep_alive = self.cfg.keepalive <= 0\n        server.start(num_processes=1)\n\n        self.ioloop.start()\n"
  },
  {
    "path": "gunicorn/workers/sync.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n#\n\nfrom datetime import datetime\nimport errno\nimport os\nimport select\nimport socket\nimport ssl\nimport sys\n\nfrom gunicorn import http\nfrom gunicorn.http import wsgi\nfrom gunicorn import sock\nfrom gunicorn import util\nfrom gunicorn.workers import base\n\n\nclass StopWaiting(Exception):\n    \"\"\" exception raised to stop waiting for a connection \"\"\"\n\n\nclass SyncWorker(base.Worker):\n\n    def accept(self, listener):\n        client, addr = listener.accept()\n        client.setblocking(1)\n        util.close_on_exec(client)\n        self.handle(listener, client, addr)\n\n    def wait(self, timeout):\n        try:\n            self.notify()\n            ret = select.select(self.wait_fds, [], [], timeout)\n            if ret[0]:\n                if self.PIPE[0] in ret[0]:\n                    os.read(self.PIPE[0], 1)\n                return ret[0]\n\n        except OSError as e:\n            if e.args[0] == errno.EINTR:\n                return self.sockets\n            if e.args[0] == errno.EBADF:\n                if self.nr < 0:\n                    return self.sockets\n                else:\n                    raise StopWaiting\n            raise\n\n    def is_parent_alive(self):\n        # If our parent changed then we shut down.\n        if self.ppid != os.getppid():\n            self.log.info(\"Parent changed, shutting down: %s\", self)\n            return False\n        return True\n\n    def run_for_one(self, timeout):\n        listener = self.sockets[0]\n        while self.alive:\n            self.notify()\n\n            # Accept a connection. If we get an error telling us\n            # that no connection is waiting we fall down to the\n            # select which is where we'll wait for a bit for new\n            # workers to come give us some love.\n            try:\n                self.accept(listener)\n                # Keep processing clients until no one is waiting. This\n                # prevents the need to select() for every client that we\n                # process.\n                continue\n\n            except OSError as e:\n                if e.errno not in (errno.EAGAIN, errno.ECONNABORTED,\n                                   errno.EWOULDBLOCK):\n                    raise\n\n            if not self.is_parent_alive():\n                return\n\n            try:\n                self.wait(timeout)\n            except StopWaiting:\n                return\n\n    def run_for_multiple(self, timeout):\n        while self.alive:\n            self.notify()\n\n            try:\n                ready = self.wait(timeout)\n            except StopWaiting:\n                return\n\n            if ready is not None:\n                for listener in ready:\n                    if listener == self.PIPE[0]:\n                        continue\n\n                    try:\n                        self.accept(listener)\n                    except OSError as e:\n                        if e.errno not in (errno.EAGAIN, errno.ECONNABORTED,\n                                           errno.EWOULDBLOCK):\n                            raise\n\n            if not self.is_parent_alive():\n                return\n\n    def run(self):\n        # if no timeout is given the worker will never wait and will\n        # use the CPU for nothing. This minimal timeout prevent it.\n        timeout = self.timeout or 0.5\n\n        # Warn if HTTP/2 is requested - sync worker doesn't support it\n        if 'h2' in self.cfg.http_protocols:\n            self.log.warning(\n                \"HTTP/2 is not supported by the sync worker. \"\n                \"Use gthread, gevent, eventlet, or asgi workers for HTTP/2 support. \"\n                \"Falling back to HTTP/1.1 only.\"\n            )\n\n        # self.socket appears to lose its blocking status after\n        # we fork in the arbiter. Reset it here.\n        for s in self.sockets:\n            s.setblocking(0)\n\n        if len(self.sockets) > 1:\n            self.run_for_multiple(timeout)\n        else:\n            self.run_for_one(timeout)\n\n    def handle(self, listener, client, addr):\n        req = None\n        try:\n            if self.cfg.is_ssl:\n                client = sock.ssl_wrap_socket(client, self.cfg)\n            parser = http.get_parser(self.cfg, client, addr)\n            req = next(parser)\n            self.handle_request(listener, req, client, addr)\n        except http.errors.NoMoreData as e:\n            self.log.debug(\"Ignored premature client disconnection. %s\", e)\n        except StopIteration as e:\n            self.log.debug(\"Closing connection. %s\", e)\n        except ssl.SSLError as e:\n            if e.args[0] == ssl.SSL_ERROR_EOF:\n                self.log.debug(\"ssl connection closed\")\n                client.close()\n            else:\n                self.log.debug(\"Error processing SSL request.\")\n                self.handle_error(req, client, addr, e)\n        except OSError as e:\n            if e.errno not in (errno.EPIPE, errno.ECONNRESET, errno.ENOTCONN):\n                self.log.exception(\"Socket error processing request.\")\n            else:\n                if e.errno == errno.ECONNRESET:\n                    self.log.debug(\"Ignoring connection reset\")\n                elif e.errno == errno.ENOTCONN:\n                    self.log.debug(\"Ignoring socket not connected\")\n                else:\n                    self.log.debug(\"Ignoring EPIPE\")\n        except BaseException as e:\n            self.handle_error(req, client, addr, e)\n        finally:\n            util.close(client)\n\n    def handle_request(self, listener, req, client, addr):\n        environ = {}\n        resp = None\n        try:\n            self.cfg.pre_request(self, req)\n            request_start = datetime.now()\n            resp, environ = wsgi.create(req, client, addr,\n                                        listener.getsockname(), self.cfg)\n            # Force the connection closed until someone shows\n            # a buffering proxy that supports Keep-Alive to\n            # the backend.\n            resp.force_close()\n            self.nr += 1\n            if self.nr >= self.max_requests:\n                self.log.info(\"Autorestarting worker after current request.\")\n                self.alive = False\n            respiter = self.wsgi(environ, resp.start_response)\n            try:\n                if isinstance(respiter, environ['wsgi.file_wrapper']):\n                    resp.write_file(respiter)\n                else:\n                    for item in respiter:\n                        resp.write(item)\n                resp.close()\n            finally:\n                request_time = datetime.now() - request_start\n                self.log.access(resp, req, environ, request_time)\n                if hasattr(respiter, \"close\"):\n                    respiter.close()\n        except OSError:\n            # pass to next try-except level\n            util.reraise(*sys.exc_info())\n        except Exception:\n            if resp and resp.headers_sent:\n                # If the requests have already been sent, we should close the\n                # connection to indicate the error.\n                self.log.exception(\"Error handling request\")\n                try:\n                    client.shutdown(socket.SHUT_RDWR)\n                    client.close()\n                except OSError:\n                    pass\n                raise StopIteration()\n            raise\n        finally:\n            try:\n                self.cfg.post_request(self, req, environ, resp)\n            except Exception:\n                self.log.exception(\"Exception in post_request hook\")\n"
  },
  {
    "path": "gunicorn/workers/workertmp.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport os\nimport time\nimport platform\nimport tempfile\n\nfrom gunicorn import util\n\nPLATFORM = platform.system()\nIS_CYGWIN = PLATFORM.startswith('CYGWIN')\n\n\nclass WorkerTmp:\n\n    def __init__(self, cfg):\n        old_umask = os.umask(cfg.umask)\n        fdir = cfg.worker_tmp_dir\n        if fdir and not os.path.isdir(fdir):\n            raise RuntimeError(\"%s doesn't exist. Can't create workertmp.\" % fdir)\n        fd, name = tempfile.mkstemp(prefix=\"wgunicorn-\", dir=fdir)\n        os.umask(old_umask)\n\n        # change the owner and group of the file if the worker will run as\n        # a different user or group, so that the worker can modify the file\n        if cfg.uid != os.geteuid() or cfg.gid != os.getegid():\n            util.chown(name, cfg.uid, cfg.gid)\n\n        # unlink the file so we don't leak temporary files\n        try:\n            if not IS_CYGWIN:\n                util.unlink(name)\n            # In Python 3.8, open() emits RuntimeWarning if buffering=1 for binary mode.\n            # Because we never write to this file, pass 0 to switch buffering off.\n            self._tmp = os.fdopen(fd, 'w+b', 0)\n        except Exception:\n            os.close(fd)\n            raise\n\n    def notify(self):\n        new_time = time.monotonic()\n        os.utime(self._tmp.fileno(), (new_time, new_time))\n\n    def last_update(self):\n        return os.fstat(self._tmp.fileno()).st_mtime\n\n    def fileno(self):\n        return self._tmp.fileno()\n\n    def close(self):\n        return self._tmp.close()\n"
  },
  {
    "path": "mkdocs.yml",
    "content": "site_name: Gunicorn\nsite_url: https://gunicorn.org\nrepo_url: https://github.com/benoitc/gunicorn\nrepo_name: benoitc/gunicorn\ndocs_dir: docs/content\nuse_directory_urls: true\n\nnav:\n  - Home: index.md\n  - Getting Started:\n      - Quickstart: quickstart.md\n      - Install: install.md\n      - Run: run.md\n      - Configure: configure.md\n  - Guides:\n      - Deploy: deploy.md\n      - Docker: guides/docker.md\n      - HTTP/2: guides/http2.md\n      - ASGI Worker: asgi.md\n      - Dirty Arbiters: dirty.md\n      - Control Interface: guides/gunicornc.md\n      - uWSGI Protocol: uwsgi.md\n      - Signals: signals.md\n      - Instrumentation: instrumentation.md\n      - Custom: custom.md\n      - Design: design.md\n  - Community:\n      - Overview: community.md\n      - FAQ: faq.md\n      - Support Us: sponsor.md\n  - Sponsor: sponsor.md\n  - Reference:\n      - Settings: reference/settings.md\n  - News:\n      - Latest: news.md\n      - '2026': 2026-news.md\n      - '2024': 2024-news.md\n      - '2023': 2023-news.md\n      - '2021': 2021-news.md\n      - '2020': 2020-news.md\n      - '2019': 2019-news.md\n      - '2018': 2018-news.md\n      - '2017': 2017-news.md\n      - '2016': 2016-news.md\n      - '2015': 2015-news.md\n      - '2014': 2014-news.md\n      - '2013': 2013-news.md\n      - '2012': 2012-news.md\n      - '2011': 2011-news.md\n      - '2010': 2010-news.md\n\ntheme:\n  name: material\n  custom_dir: overrides\n  language: en\n  logo: assets/gunicorn.svg\n  favicon: assets/gunicorn.svg\n  palette:\n    - media: \"(prefers-color-scheme: light)\"\n      scheme: default\n      primary: green\n      accent: teal\n      toggle:\n        icon: material/brightness-7\n        name: Switch to dark mode\n    - media: \"(prefers-color-scheme: dark)\"\n      scheme: slate\n      primary: green\n      accent: teal\n      toggle:\n        icon: material/brightness-4\n        name: Switch to light mode\n  font:\n    text: Inter\n    code: JetBrains Mono\n  features:\n    - content.code.copy\n    - content.code.annotate\n    - navigation.instant\n    - navigation.instant.progress\n    - navigation.tracking\n    - navigation.sections\n    - navigation.tabs\n    - navigation.tabs.sticky\n    - navigation.top\n    - navigation.path\n    - search.highlight\n    - search.suggest\n    - search.share\n    - toc.follow\n    - toc.integrate\n  icon:\n    repo: fontawesome/brands/github\n\nplugins:\n  - search\n  - macros\n  - gen-files:\n      scripts:\n        - scripts/build_settings_doc.py\n\nmarkdown_extensions:\n  - admonition\n  - attr_list\n  - def_list\n  - footnotes\n  - md_in_html\n  - tables\n  - markdown_grid_tables:\n      hard_linebreaks: true\n  - toc:\n      permalink: true\n  - pymdownx.details\n  - pymdownx.highlight\n  - pymdownx.inlinehilite\n  - pymdownx.magiclink\n  - pymdownx.superfences\n  - pymdownx.snippets:\n      base_path:\n        - .\n      check_paths: true\n  - pymdownx.tabbed:\n      alternate_style: true\n  - pymdownx.tasklist:\n      custom_checkbox: true\n\nextra_css:\n  - styles/overrides.css\n  - assets/stylesheets/home.css\n\nextra_javascript:\n  - assets/javascripts/toc-collapse.js\n\nextra:\n  social:\n    - icon: fontawesome/brands/github\n      link: https://github.com/benoitc/gunicorn\n    - icon: fontawesome/brands/python\n      link: https://pypi.org/project/gunicorn/\n    - icon: fontawesome/solid/heart\n      link: https://github.com/sponsors/benoitc\n"
  },
  {
    "path": "overrides/home.html",
    "content": "{% extends \"main.html\" %}\n\n{% block tabs %}\n{{ super() }}\n{% endblock %}\n\n{% block htmltitle %}\n<title>Gunicorn - Python WSGI HTTP Server for UNIX</title>\n{% endblock %}\n\n{% block styles %}\n{{ super() }}\n<link rel=\"stylesheet\" href=\"{{ 'assets/stylesheets/home.css' | url }}\">\n<style>\n  /* Hide sidebar on desktop, keep drawer for mobile/tablet */\n  @media screen and (min-width: 76.25em) {\n    .md-sidebar--primary { display: none; }\n  }\n  .md-sidebar--secondary { display: none; }\n</style>\n{% endblock %}\n\n{% block hero %}{% endblock %}\n\n{% block content %}{% endblock %}\n\n{% block site_nav %}\n  {{ super() }}\n{% endblock %}\n\n{% block container %}\n<main class=\"home\">\n  {{ page.content }}\n</main>\n{% endblock %}\n\n{% block footer %}\n{{ super() }}\n{% endblock %}\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools>=61.2\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\n# see https://packaging.python.org/en/latest/specifications/pyproject-toml/\nname = \"gunicorn\"\nauthors = [{name = \"Benoit Chesneau\", email = \"benoitc@gunicorn.org\"}]\nlicense = \"MIT\"\nlicense-files = [\"LICENSE\"]\ndescription = \"WSGI HTTP Server for UNIX\"\nreadme = \"README.md\"\nclassifiers = [\n    \"Development Status :: 5 - Production/Stable\",\n    \"Environment :: Other Environment\",\n    \"Intended Audience :: Developers\",\n    \"Operating System :: MacOS :: MacOS X\",\n    \"Operating System :: POSIX\",\n    \"Programming Language :: Python\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Programming Language :: Python :: 3 :: Only\",\n    \"Programming Language :: Python :: Implementation :: CPython\",\n    \"Programming Language :: Python :: Implementation :: PyPy\",\n    \"Topic :: Internet\",\n    \"Topic :: Utilities\",\n    \"Topic :: Software Development :: Libraries :: Python Modules\",\n    \"Topic :: Internet :: WWW/HTTP\",\n    \"Topic :: Internet :: WWW/HTTP :: WSGI\",\n    \"Topic :: Internet :: WWW/HTTP :: WSGI :: Server\",\n    \"Topic :: Internet :: WWW/HTTP :: Dynamic Content\",\n]\nrequires-python = \">=3.10\"\ndependencies = [\n    \"packaging\",\n]\ndynamic = [\"version\"]\n\n[project.urls]\nHomepage = \"https://gunicorn.org\"\nDocumentation = \"https://gunicorn.org\"\n\"Issue tracker\" = \"https://github.com/benoitc/gunicorn/issues\"\n\"Source code\" = \"https://github.com/benoitc/gunicorn\"\nChangelog = \"https://gunicorn.org/news/\"\n\n[project.optional-dependencies]\ngevent = [\"gevent>=24.10.1\"]\neventlet = [\"eventlet>=0.40.3\"]\ntornado = [\"tornado>=6.5.0\"]\ngthread = []\nsetproctitle = [\"setproctitle\"]\nhttp2 = [\"h2>=4.1.0\"]\ntesting = [\n    \"gevent>=24.10.1\",\n    \"eventlet>=0.40.3\",\n    \"h2>=4.1.0\",\n    \"coverage\",\n    \"pytest\",\n    \"pytest-cov\",\n    \"pytest-asyncio\",\n    \"uvloop>=0.19.0\",\n    \"httpx[http2]\",\n]\n\n[project.scripts]\n# duplicates \"python -m gunicorn\" handling in __main__.py\ngunicorn = \"gunicorn.app.wsgiapp:run\"\ngunicornc = \"gunicorn.ctl.cli:main\"\n\n# note the quotes around \"paste.server_runner\" to escape the dot\n[project.entry-points.\"paste.server_runner\"]\nmain = \"gunicorn.app.pasterapp:serve\"\n\n[tool.pytest.ini_options]\n# # can override these: python -m pytest --override-ini=\"addopts=\"\nnorecursedirs = [\"examples\", \"lib\", \"local\", \"src\", \"tests/docker\"]\ntestpaths = [\"tests/\"]\naddopts = \"--assert=plain --cov=gunicorn --cov-report=xml\"\nfilterwarnings = [\n    # Eventlet patches select module, which breaks asyncio event loop cleanup\n    # This is expected behavior when testing eventlet worker\n    \"ignore::pytest.PytestUnraisableExceptionWarning\",\n]\n\n[tool.setuptools]\nzip-safe = false\ninclude-package-data = true\n\n[tool.setuptools.packages]\nfind = {namespaces = false}\n\n[tool.setuptools.dynamic]\nversion = {attr = \"gunicorn.__version__\"}\n"
  },
  {
    "path": "requirements_dev.txt",
    "content": "-r requirements_test.txt\n\n# setuptools v68.0 fails hard on invalid pyproject.toml\n# which a developer would want to know\n# otherwise, oldest known-working version is 61.2\nsetuptools>=68.0\n\nmkdocs>=1.6\nmkdocs-material>=9.5\nmkdocs-gen-files>=0.5\nmarkdown-grid-tables>=0.6\nmkdocs-macros-plugin>=1.0\npymdown-extensions>=10.0\n"
  },
  {
    "path": "requirements_test.txt",
    "content": "gevent\neventlet\ncoverage\npytest>=7.2.0\npytest-cov\npytest-asyncio\n"
  },
  {
    "path": "scripts/build_settings_doc.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Generate the Markdown settings reference for MkDocs.\"\"\"\nfrom __future__ import annotations\n\nimport inspect\nimport textwrap\nfrom pathlib import Path\nfrom typing import List\n\nimport re\n\nimport gunicorn.config as guncfg\n\nHEAD = \"\"\"\\\n> **Generated file** — update `gunicorn/config.py` instead.\n\n# Settings\n\nThis reference is built directly from `gunicorn.config.KNOWN_SETTINGS` and is\nregenerated during every documentation build.\n\n!!! note\n    Settings can be provided through the `GUNICORN_CMD_ARGS` environment\n    variable. For example:\n\n    ```console\n    $ GUNICORN_CMD_ARGS=\"--bind=127.0.0.1 --workers=3\" gunicorn app:app\n    ```\n\n    _Added in 19.7._\n\n\"\"\"\n\n\ndef _format_default(setting: guncfg.Setting) -> tuple[str, bool]:\n    if hasattr(setting, \"default_doc\"):\n        text = textwrap.dedent(setting.default_doc).strip(\"\\n\")\n        return text, True\n    default = setting.default\n    if callable(default):\n        source = textwrap.dedent(inspect.getsource(default)).strip(\"\\n\")\n        return f\"```python\\n{source}\\n```\", True\n    if default == \"\":\n        return \"`''`\", False\n    return f\"`{default!r}`\", False\n\n\ndef _format_cli(setting: guncfg.Setting) -> str | None:\n    if not setting.cli:\n        return None\n    if setting.meta:\n        variants = [f\"`{opt} {setting.meta}`\" for opt in setting.cli]\n    else:\n        variants = [f\"`{opt}`\" for opt in setting.cli]\n    return \", \".join(variants)\n\n\nREF_MAP = {\n    \"forwarded-allow-ips\": (\"reference/settings.md\", \"forwarded_allow_ips\"),\n    \"forwarder-headers\": (\"reference/settings.md\", \"forwarder_headers\"),\n    \"proxy-allow-ips\": (\"reference/settings.md\", \"proxy_allow_ips\"),\n    \"worker-class\": (\"reference/settings.md\", \"worker_class\"),\n    \"reload\": (\"reference/settings.md\", \"reload\"),\n    \"raw-env\": (\"reference/settings.md\", \"raw_env\"),\n    \"check-config\": (\"reference/settings.md\", \"check_config\"),\n    \"errorlog\": (\"reference/settings.md\", \"errorlog\"),\n    \"logconfig\": (\"reference/settings.md\", \"logconfig\"),\n    \"logconfig-json\": (\"reference/settings.md\", \"logconfig_json\"),\n    \"ssl-context\": (\"reference/settings.md\", \"ssl_context\"),\n    \"ssl-version\": (\"reference/settings.md\", \"ssl_version\"),\n    \"blocking-os-fchmod\": (\"reference/settings.md\", \"blocking_os_fchmod\"),\n    \"configuration_file\": (\"../configure.md\", \"configuration-file\"),\n}\n\nREF_PATTERN = re.compile(r\":ref:`([^`]+)`\")\n\n\ndef _convert_refs(text: str) -> str:\n    def repl(match: re.Match[str]) -> str:\n        raw = match.group(1)\n        if \"<\" in raw and raw.endswith(\">\"):\n            label, target = raw.split(\"<\", 1)\n            target = target[:-1]\n            label = label.replace(\"\\n\", \" \").strip()\n        else:\n            label, target = None, raw.strip()\n        info = REF_MAP.get(target)\n        if not info:\n            return (label or target).replace(\"\\n\", \" \").strip()\n        path, anchor = info\n        if path.endswith(\".md\"):\n            if path == \"reference/settings.md\" and anchor:\n                href = f\"#{anchor}\"\n            else:\n                href = path + (f\"#{anchor}\" if anchor else \"\")\n        else:\n            href = path + (f\"#{anchor}\" if anchor else \"\")\n        text = (label or target).replace(\"\\n\", \" \").strip()\n        return f\"[{text}]({href})\"\n\n    return REF_PATTERN.sub(repl, text)\n\n\ndef _consume_indented(lines: List[str], start: int) -> tuple[str, int]:\n    body: List[str] = []\n    i = start\n    while i < len(lines):\n        line = lines[i]\n        if line.startswith(\"   \") or not line.strip():\n            body.append(line)\n            i += 1\n        else:\n            break\n    text = textwrap.dedent(\"\\n\".join(body)).strip(\"\\n\")\n    return text, i\n\n\ndef _convert_desc(desc: str) -> str:\n    raw_lines = textwrap.dedent(desc).splitlines()\n    output: List[str] = []\n    i = 0\n    while i < len(raw_lines):\n        line = raw_lines[i]\n        stripped = line.strip()\n        if stripped.startswith(\".. note::\"):\n            body, i = _consume_indented(raw_lines, i + 1)\n            output.append(\"!!! note\")\n            if body:\n                for body_line in body.splitlines():\n                    output.append(f\"    {body_line}\" if body_line else \"\")\n            output.append(\"\")\n            continue\n        if stripped.startswith(\".. warning::\"):\n            body, i = _consume_indented(raw_lines, i + 1)\n            output.append(\"!!! warning\")\n            if body:\n                for body_line in body.splitlines():\n                    output.append(f\"    {body_line}\" if body_line else \"\")\n            output.append(\"\")\n            continue\n        if stripped.startswith(\".. deprecated::\"):\n            version = stripped.split(\"::\", 1)[1].strip()\n            body, i = _consume_indented(raw_lines, i + 1)\n            title = f\"Deprecated in {version}\" if version else \"Deprecated\"\n            output.append(f\"!!! danger \\\"{title}\\\"\")\n            if body:\n                for body_line in body.splitlines():\n                    output.append(f\"    {body_line}\" if body_line else \"\")\n            output.append(\"\")\n            continue\n        if stripped.startswith(\".. versionadded::\"):\n            version = stripped.split(\"::\", 1)[1].strip()\n            body, i = _consume_indented(raw_lines, i + 1)\n            title = f\"Added in {version}\" if version else \"Added\"\n            output.append(f\"!!! info \\\"{title}\\\"\")\n            if body:\n                for body_line in body.splitlines():\n                    output.append(f\"    {body_line}\" if body_line else \"\")\n            output.append(\"\")\n            continue\n        if stripped.startswith(\".. versionchanged::\"):\n            version = stripped.split(\"::\", 1)[1].strip()\n            body, i = _consume_indented(raw_lines, i + 1)\n            title = f\"Changed in {version}\" if version else \"Changed\"\n            output.append(f\"!!! info \\\"{title}\\\"\")\n            if body:\n                for body_line in body.splitlines():\n                    output.append(f\"    {body_line}\" if body_line else \"\")\n            output.append(\"\")\n            continue\n        if stripped.startswith(\".. code::\") or stripped.startswith(\".. code-block::\"):\n            language = stripped.split(\"::\", 1)[1].strip()\n            body, i = _consume_indented(raw_lines, i + 1)\n            fence = language or \"text\"\n            output.append(f\"```{fence}\")\n            if body:\n                output.append(body)\n            output.append(\"```\")\n            output.append(\"\")\n            continue\n\n        output.append(line)\n        i += 1\n\n    text = \"\\n\".join(output)\n    text = _convert_refs(text)\n    # Collapse excessive blank lines\n    text = re.sub(r\"\\n{3,}\", \"\\n\\n\", text)\n    return text.strip(\"\\n\")\n\n\ndef _format_setting(setting: guncfg.Setting) -> str:\n    lines: list[str] = [f\"### `{setting.name}`\", \"\"]\n\n    cli = _format_cli(setting)\n    if cli:\n        lines.extend((f\"**Command line:** {cli}\", \"\"))\n\n    default_text, is_block = _format_default(setting)\n    if is_block:\n        lines.append(\"**Default:**\")\n        lines.append(\"\")\n        lines.append(default_text)\n    else:\n        lines.append(f\"**Default:** {default_text}\")\n    lines.append(\"\")\n\n    desc = _convert_desc(setting.desc)\n    if desc:\n        lines.append(desc)\n        lines.append(\"\")\n\n    return \"\\n\".join(lines)\n\n\ndef render_settings() -> str:\n    sections: list[str] = [HEAD, '<span id=\"blocking_os_fchmod\"></span>', \"\"]\n    known_settings = sorted(guncfg.KNOWN_SETTINGS, key=lambda s: s.section)\n    current_section: str | None = None\n\n    for setting in known_settings:\n        if setting.section != current_section:\n            current_section = setting.section\n            sections.append(f\"## {current_section}\\n\")\n        sections.append(_format_setting(setting))\n\n    return \"\\n\".join(sections).strip() + \"\\n\"\n\n\ndef _write_output(markdown: str) -> None:\n    try:\n        import mkdocs_gen_files  # type: ignore\n    except ImportError:\n        mkdocs_gen_files = None\n\n    if mkdocs_gen_files is not None:\n        try:\n            with mkdocs_gen_files.open(\"reference/settings.md\", \"w\") as fh:\n                fh.write(markdown)\n                return\n        except Exception:\n            pass\n\n    output = Path(__file__).resolve().parents[1] / \"docs\" / \"content\" / \"reference\" / \"settings.md\"\n    output.parent.mkdir(parents=True, exist_ok=True)\n    output.write_text(markdown, encoding=\"utf-8\")\n\n\ndef main() -> None:\n    markdown = render_settings()\n    _write_output(markdown)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "scripts/update_thanks.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n#!/usr/bin/env python\n# Usage: git log --format=\"%an <%ae>\" | python update_thanks.py\n# You will get a result.txt file, you can work with the file (update, remove, ...)\n#\n# Install\n# =======\n# pip install validate_email pyDNS\n#\nimport sys\n\nfrom validate_email import validate_email\nfrom email.utils import parseaddr\nimport DNS.Base\n\naddresses = set()\nbad_addresses = set()\ncollection = []\n\nlines = list(reversed(sys.stdin.readlines()))\n\nfor author in map(str.strip, lines):\n    realname, email_address = parseaddr(author)\n\n    if email_address not in addresses:\n        if email_address in bad_addresses:\n            continue\n        else:\n            try:\n                value = validate_email(email_address)\n                if value:\n                    addresses.add(email_address)\n                    collection.append(author)\n                else:\n                    bad_addresses.add(email_address)\n            except DNS.Base.TimeoutError:\n                bad_addresses.add(email_address)\n\n\nwith open('result.txt', 'w') as output:\n    output.write('\\n'.join(collection))\n"
  },
  {
    "path": "tests/config/__init__.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n"
  },
  {
    "path": "tests/config/test_cfg.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nbind = \"unix:/tmp/bar/baz\"\nworkers = 3\nproc_name = \"fooey\"\ndefault_proc_name = \"blurgh\"\n"
  },
  {
    "path": "tests/config/test_cfg_alt.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nproc_name = \"not-fooey\"\n"
  },
  {
    "path": "tests/config/test_cfg_with_wsgi_app.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nwsgi_app = \"app1:app1\"\n"
  },
  {
    "path": "tests/conftest.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Pytest configuration for gunicorn tests.\"\"\"\n\nimport os\nimport sys\n\n# Add the tests directory to sys.path so test support modules can be imported\n# as 'tests.module_name' (e.g., 'tests.support_dirty_apps:CounterApp')\ntests_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\nif tests_dir not in sys.path:\n    sys.path.insert(0, tests_dir)\n"
  },
  {
    "path": "tests/ctl/__init__.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n"
  },
  {
    "path": "tests/ctl/test_client.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Tests for control socket client.\"\"\"\n\nimport os\nimport socket\nimport tempfile\nimport threading\n\nimport pytest\n\nfrom gunicorn.ctl.client import (\n    ControlClient,\n    ControlClientError,\n    parse_command,\n)\nfrom gunicorn.ctl.protocol import ControlProtocol, make_response\n\n\nclass TestControlClientInit:\n    \"\"\"Tests for ControlClient initialization.\"\"\"\n\n    def test_init_attributes(self):\n        \"\"\"Test that client is initialized with correct attributes.\"\"\"\n        client = ControlClient(\"/tmp/test.sock\", timeout=60.0)\n\n        assert client.socket_path == \"/tmp/test.sock\"\n        assert client.timeout == 60.0\n        assert client._sock is None\n        assert client._request_id == 0\n\n\nclass TestControlClientConnect:\n    \"\"\"Tests for ControlClient connection.\"\"\"\n\n    def test_connect_nonexistent_socket(self):\n        \"\"\"Test connecting to non-existent socket.\"\"\"\n        client = ControlClient(\"/nonexistent/socket.sock\")\n\n        with pytest.raises(ControlClientError) as exc_info:\n            client.connect()\n\n        assert \"Failed to connect\" in str(exc_info.value)\n\n    def test_connect_success(self):\n        \"\"\"Test successful connection.\"\"\"\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"test.sock\")\n\n            # Create a listening socket\n            server_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)\n            server_sock.bind(socket_path)\n            server_sock.listen(1)\n\n            try:\n                client = ControlClient(socket_path)\n                client.connect()\n\n                assert client._sock is not None\n                client.close()\n            finally:\n                server_sock.close()\n\n    def test_connect_already_connected(self):\n        \"\"\"Test that connect is idempotent.\"\"\"\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"test.sock\")\n\n            server_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)\n            server_sock.bind(socket_path)\n            server_sock.listen(1)\n\n            try:\n                client = ControlClient(socket_path)\n                client.connect()\n                first_sock = client._sock\n                client.connect()  # Should not create new connection\n\n                assert client._sock is first_sock\n                client.close()\n            finally:\n                server_sock.close()\n\n\nclass TestControlClientClose:\n    \"\"\"Tests for ControlClient close.\"\"\"\n\n    def test_close_idempotent(self):\n        \"\"\"Test that close can be called multiple times.\"\"\"\n        client = ControlClient(\"/tmp/test.sock\")\n        client.close()\n        client.close()  # Should not raise\n\n    def test_close_clears_socket(self):\n        \"\"\"Test that close clears the socket.\"\"\"\n        client = ControlClient(\"/tmp/test.sock\")\n        client._sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)\n        client.close()\n\n        assert client._sock is None\n\n\nclass TestControlClientContextManager:\n    \"\"\"Tests for context manager functionality.\"\"\"\n\n    def test_context_manager_connection_error(self):\n        \"\"\"Test context manager with connection error.\"\"\"\n        client = ControlClient(\"/nonexistent/socket.sock\")\n\n        with pytest.raises(ControlClientError):\n            with client:\n                pass\n\n    def test_context_manager_success(self):\n        \"\"\"Test successful context manager usage.\"\"\"\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"test.sock\")\n\n            server_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)\n            server_sock.bind(socket_path)\n            server_sock.listen(1)\n\n            try:\n                with ControlClient(socket_path) as client:\n                    assert client._sock is not None\n\n                # After context manager exits, socket should be closed\n                assert client._sock is None\n            finally:\n                server_sock.close()\n\n\nclass TestControlClientSendCommand:\n    \"\"\"Tests for send_command functionality.\"\"\"\n\n    def test_send_command_success(self):\n        \"\"\"Test successful command send.\"\"\"\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"test.sock\")\n\n            server_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)\n            server_sock.bind(socket_path)\n            server_sock.listen(1)\n\n            response_data = {\"workers\": [], \"count\": 0}\n            response_sent = threading.Event()\n\n            def server_handler():\n                conn, _ = server_sock.accept()\n                try:\n                    msg = ControlProtocol.read_message(conn)\n                    resp = make_response(msg[\"id\"], response_data)\n                    ControlProtocol.write_message(conn, resp)\n                    response_sent.set()\n                finally:\n                    conn.close()\n\n            server_thread = threading.Thread(target=server_handler)\n            server_thread.start()\n\n            try:\n                client = ControlClient(socket_path, timeout=5.0)\n                result = client.send_command(\"show workers\")\n\n                assert result == response_data\n                client.close()\n            finally:\n                response_sent.wait(timeout=2.0)\n                server_thread.join(timeout=2.0)\n                server_sock.close()\n\n    def test_send_command_error_response(self):\n        \"\"\"Test handling error response.\"\"\"\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"test.sock\")\n\n            server_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)\n            server_sock.bind(socket_path)\n            server_sock.listen(1)\n\n            def server_handler():\n                conn, _ = server_sock.accept()\n                try:\n                    msg = ControlProtocol.read_message(conn)\n                    resp = {\n                        \"id\": msg[\"id\"],\n                        \"status\": \"error\",\n                        \"error\": \"Unknown command\",\n                    }\n                    ControlProtocol.write_message(conn, resp)\n                finally:\n                    conn.close()\n\n            server_thread = threading.Thread(target=server_handler)\n            server_thread.start()\n\n            try:\n                client = ControlClient(socket_path, timeout=5.0)\n\n                with pytest.raises(ControlClientError) as exc_info:\n                    client.send_command(\"invalid command\")\n\n                assert \"Unknown command\" in str(exc_info.value)\n                client.close()\n            finally:\n                server_thread.join(timeout=2.0)\n                server_sock.close()\n\n    def test_send_command_auto_connect(self):\n        \"\"\"Test that send_command auto-connects if not connected.\"\"\"\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"test.sock\")\n\n            server_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)\n            server_sock.bind(socket_path)\n            server_sock.listen(1)\n\n            def server_handler():\n                conn, _ = server_sock.accept()\n                try:\n                    msg = ControlProtocol.read_message(conn)\n                    resp = make_response(msg[\"id\"], {})\n                    ControlProtocol.write_message(conn, resp)\n                finally:\n                    conn.close()\n\n            server_thread = threading.Thread(target=server_handler)\n            server_thread.start()\n\n            try:\n                client = ControlClient(socket_path, timeout=5.0)\n                # Don't call connect() explicitly\n                result = client.send_command(\"help\")\n\n                assert isinstance(result, dict)\n                client.close()\n            finally:\n                server_thread.join(timeout=2.0)\n                server_sock.close()\n\n\nclass TestParseCommand:\n    \"\"\"Tests for command parsing.\"\"\"\n\n    def test_parse_simple_command(self):\n        \"\"\"Test parsing simple command.\"\"\"\n        cmd, args = parse_command(\"show workers\")\n        assert cmd == \"show workers\"\n        assert args == []\n\n    def test_parse_command_with_args(self):\n        \"\"\"Test parsing command with arguments.\"\"\"\n        cmd, args = parse_command(\"worker add 2\")\n        assert cmd == \"worker add\"\n        assert args == [\"2\"]\n\n    def test_parse_command_with_multiple_args(self):\n        \"\"\"Test parsing command with multiple arguments.\"\"\"\n        cmd, args = parse_command(\"worker kill 12345\")\n        assert cmd == \"worker kill\"\n        assert args == [\"12345\"]\n\n    def test_parse_empty_command(self):\n        \"\"\"Test parsing empty command.\"\"\"\n        cmd, args = parse_command(\"\")\n        assert cmd == \"\"\n        assert args == []\n\n    def test_parse_command_quoted(self):\n        \"\"\"Test parsing command with quoted arguments.\"\"\"\n        cmd, args = parse_command('worker kill \"12345\"')\n        assert cmd == \"worker kill\"\n        assert args == [\"12345\"]\n"
  },
  {
    "path": "tests/ctl/test_handlers.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Tests for control socket command handlers.\"\"\"\n\nimport signal\nimport time\nfrom unittest.mock import MagicMock, patch\n\nfrom gunicorn.ctl.handlers import CommandHandlers\n\n\nclass MockWorker:\n    \"\"\"Mock worker for testing.\"\"\"\n\n    def __init__(self, pid, age, booted=True, aborted=False):\n        self.pid = pid\n        self.age = age\n        self.booted = booted\n        self.aborted = aborted\n        self.tmp = MagicMock()\n        self.tmp.last_update.return_value = time.monotonic()\n\n\nclass MockListener:\n    \"\"\"Mock listener for testing.\"\"\"\n\n    def __init__(self, address, fd=3):\n        self._address = address\n        self._fd = fd\n        self.sock = MagicMock()\n        self.sock.family = 2  # AF_INET\n\n    def __str__(self):\n        return self._address\n\n    def fileno(self):\n        return self._fd\n\n\nclass MockConfig:\n    \"\"\"Mock config for testing.\"\"\"\n\n    def __init__(self):\n        self.bind = ['127.0.0.1:8000']\n        self.workers = 4\n        self.worker_class = 'sync'\n        self.threads = 1\n        self.timeout = 30\n        self.graceful_timeout = 30\n        self.keepalive = 2\n        self.max_requests = 0\n        self.max_requests_jitter = 0\n        self.worker_connections = 1000\n        self.preload_app = False\n        self.daemon = False\n        self.pidfile = None\n        self.proc_name = 'test_app'\n        self.reload = False\n        self.dirty_workers = 0\n        self.dirty_apps = []\n        self.dirty_timeout = 30\n        self.control_socket = 'gunicorn.ctl'\n        self.control_socket_disable = False\n\n\nclass MockArbiter:\n    \"\"\"Mock arbiter for testing.\"\"\"\n\n    def __init__(self):\n        self.cfg = MockConfig()\n        self.pid = 12345\n        self.WORKERS = {}\n        self.LISTENERS = []\n        self.dirty_arbiter_pid = 0\n        self.dirty_arbiter = None\n        self.num_workers = 4\n        self._stats = {\n            'start_time': time.time() - 3600,  # 1 hour ago\n            'workers_spawned': 10,\n            'workers_killed': 5,\n            'reloads': 2,\n        }\n\n    def wakeup(self):\n        pass\n\n\nclass TestShowWorkers:\n    \"\"\"Tests for show workers command.\"\"\"\n\n    def test_show_workers_empty(self):\n        \"\"\"Test showing workers when none exist.\"\"\"\n        arbiter = MockArbiter()\n        handlers = CommandHandlers(arbiter)\n\n        result = handlers.show_workers()\n\n        assert result[\"workers\"] == []\n        assert result[\"count\"] == 0\n\n    def test_show_workers_with_workers(self):\n        \"\"\"Test showing workers.\"\"\"\n        arbiter = MockArbiter()\n        arbiter.WORKERS = {\n            1001: MockWorker(1001, 1),\n            1002: MockWorker(1002, 2),\n            1003: MockWorker(1003, 3),\n        }\n        handlers = CommandHandlers(arbiter)\n\n        result = handlers.show_workers()\n\n        assert result[\"count\"] == 3\n        assert len(result[\"workers\"]) == 3\n\n        # Verify sorted by age\n        ages = [w[\"age\"] for w in result[\"workers\"]]\n        assert ages == sorted(ages)\n\n        # Verify worker data\n        worker = result[\"workers\"][0]\n        assert \"pid\" in worker\n        assert \"age\" in worker\n        assert \"booted\" in worker\n        assert \"last_heartbeat\" in worker\n\n\nclass TestShowStats:\n    \"\"\"Tests for show stats command.\"\"\"\n\n    def test_show_stats(self):\n        \"\"\"Test showing stats.\"\"\"\n        arbiter = MockArbiter()\n        arbiter.WORKERS = {\n            1001: MockWorker(1001, 1),\n            1002: MockWorker(1002, 2),\n        }\n        handlers = CommandHandlers(arbiter)\n\n        result = handlers.show_stats()\n\n        assert result[\"pid\"] == 12345\n        assert result[\"workers_current\"] == 2\n        assert result[\"workers_target\"] == 4\n        assert result[\"workers_spawned\"] == 10\n        assert result[\"workers_killed\"] == 5\n        assert result[\"reloads\"] == 2\n        assert result[\"uptime\"] is not None\n        assert result[\"uptime\"] > 0\n\n\nclass TestShowConfig:\n    \"\"\"Tests for show config command.\"\"\"\n\n    def test_show_config(self):\n        \"\"\"Test showing config.\"\"\"\n        arbiter = MockArbiter()\n        handlers = CommandHandlers(arbiter)\n\n        result = handlers.show_config()\n\n        assert result[\"workers\"] == 4\n        assert result[\"timeout\"] == 30\n        assert result[\"bind\"] == ['127.0.0.1:8000']\n\n\nclass TestShowListeners:\n    \"\"\"Tests for show listeners command.\"\"\"\n\n    def test_show_listeners_empty(self):\n        \"\"\"Test showing listeners when none exist.\"\"\"\n        arbiter = MockArbiter()\n        handlers = CommandHandlers(arbiter)\n\n        result = handlers.show_listeners()\n\n        assert result[\"listeners\"] == []\n        assert result[\"count\"] == 0\n\n    def test_show_listeners(self):\n        \"\"\"Test showing listeners.\"\"\"\n        arbiter = MockArbiter()\n        arbiter.LISTENERS = [\n            MockListener(\"127.0.0.1:8000\", fd=3),\n            MockListener(\"127.0.0.1:8001\", fd=4),\n        ]\n        handlers = CommandHandlers(arbiter)\n\n        result = handlers.show_listeners()\n\n        assert result[\"count\"] == 2\n        assert len(result[\"listeners\"]) == 2\n        assert result[\"listeners\"][0][\"address\"] == \"127.0.0.1:8000\"\n\n\nclass TestWorkerAdd:\n    \"\"\"Tests for worker add command.\"\"\"\n\n    def test_worker_add_default(self):\n        \"\"\"Test adding one worker (default).\"\"\"\n        arbiter = MockArbiter()\n        arbiter.wakeup = MagicMock()\n        handlers = CommandHandlers(arbiter)\n\n        result = handlers.worker_add()\n\n        assert result[\"added\"] == 1\n        assert result[\"previous\"] == 4\n        assert result[\"total\"] == 5\n        assert arbiter.num_workers == 5\n        arbiter.wakeup.assert_called_once()\n\n    def test_worker_add_multiple(self):\n        \"\"\"Test adding multiple workers.\"\"\"\n        arbiter = MockArbiter()\n        arbiter.wakeup = MagicMock()\n        handlers = CommandHandlers(arbiter)\n\n        result = handlers.worker_add(3)\n\n        assert result[\"added\"] == 3\n        assert result[\"total\"] == 7\n\n\nclass TestWorkerRemove:\n    \"\"\"Tests for worker remove command.\"\"\"\n\n    def test_worker_remove_default(self):\n        \"\"\"Test removing one worker (default).\"\"\"\n        arbiter = MockArbiter()\n        arbiter.wakeup = MagicMock()\n        handlers = CommandHandlers(arbiter)\n\n        result = handlers.worker_remove()\n\n        assert result[\"removed\"] == 1\n        assert result[\"previous\"] == 4\n        assert result[\"total\"] == 3\n        assert arbiter.num_workers == 3\n        arbiter.wakeup.assert_called_once()\n\n    def test_worker_remove_cannot_go_below_one(self):\n        \"\"\"Test that worker count cannot go below 1.\"\"\"\n        arbiter = MockArbiter()\n        arbiter.num_workers = 2\n        arbiter.wakeup = MagicMock()\n        handlers = CommandHandlers(arbiter)\n\n        result = handlers.worker_remove(5)\n\n        assert result[\"removed\"] == 1\n        assert result[\"total\"] == 1\n        assert arbiter.num_workers == 1\n\n\nclass TestWorkerKill:\n    \"\"\"Tests for worker kill command.\"\"\"\n\n    def test_worker_kill_success(self):\n        \"\"\"Test killing a worker.\"\"\"\n        arbiter = MockArbiter()\n        arbiter.WORKERS = {1001: MockWorker(1001, 1)}\n        handlers = CommandHandlers(arbiter)\n\n        with patch('os.kill') as mock_kill:\n            result = handlers.worker_kill(1001)\n\n        assert result[\"success\"] is True\n        assert result[\"killed\"] == 1001\n        mock_kill.assert_called_once_with(1001, signal.SIGTERM)\n\n    def test_worker_kill_not_found(self):\n        \"\"\"Test killing a non-existent worker.\"\"\"\n        arbiter = MockArbiter()\n        handlers = CommandHandlers(arbiter)\n\n        result = handlers.worker_kill(9999)\n\n        assert result[\"success\"] is False\n        assert \"not found\" in result[\"error\"]\n\n\nclass TestShowDirty:\n    \"\"\"Tests for show dirty command.\"\"\"\n\n    def test_show_dirty_disabled(self):\n        \"\"\"Test showing dirty when disabled.\"\"\"\n        arbiter = MockArbiter()\n        handlers = CommandHandlers(arbiter)\n\n        result = handlers.show_dirty()\n\n        assert result[\"enabled\"] is False\n        assert result[\"pid\"] is None\n\n\nclass TestDirtyAdd:\n    \"\"\"Tests for dirty add command.\"\"\"\n\n    def test_dirty_add_not_running(self):\n        \"\"\"Test dirty add when dirty arbiter not running.\"\"\"\n        arbiter = MockArbiter()\n        handlers = CommandHandlers(arbiter)\n\n        result = handlers.dirty_add()\n\n        assert result[\"success\"] is False\n        assert \"not running\" in result[\"error\"]\n\n    def test_dirty_add_no_socket(self):\n        \"\"\"Test dirty add when socket path not available.\"\"\"\n        arbiter = MockArbiter()\n        arbiter.dirty_arbiter_pid = 2000\n        handlers = CommandHandlers(arbiter)\n\n        # No dirty_arbiter attribute and no env var\n        with patch.dict('os.environ', {}, clear=True):\n            result = handlers.dirty_add()\n\n        assert result[\"success\"] is False\n        assert \"socket\" in result[\"error\"].lower()\n\n\nclass TestDirtyRemove:\n    \"\"\"Tests for dirty remove command.\"\"\"\n\n    def test_dirty_remove_not_running(self):\n        \"\"\"Test dirty remove when dirty arbiter not running.\"\"\"\n        arbiter = MockArbiter()\n        handlers = CommandHandlers(arbiter)\n\n        result = handlers.dirty_remove()\n\n        assert result[\"success\"] is False\n        assert \"not running\" in result[\"error\"]\n\n    def test_dirty_remove_no_socket(self):\n        \"\"\"Test dirty remove when socket path not available.\"\"\"\n        arbiter = MockArbiter()\n        arbiter.dirty_arbiter_pid = 2000\n        handlers = CommandHandlers(arbiter)\n\n        # No dirty_arbiter attribute and no env var\n        with patch.dict('os.environ', {}, clear=True):\n            result = handlers.dirty_remove()\n\n        assert result[\"success\"] is False\n        assert \"socket\" in result[\"error\"].lower()\n\n\nclass TestReload:\n    \"\"\"Tests for reload command.\"\"\"\n\n    def test_reload(self):\n        \"\"\"Test reload command.\"\"\"\n        arbiter = MockArbiter()\n        handlers = CommandHandlers(arbiter)\n\n        with patch('os.kill') as mock_kill:\n            result = handlers.reload()\n\n        assert result[\"status\"] == \"reloading\"\n        mock_kill.assert_called_once_with(12345, signal.SIGHUP)\n\n\nclass TestReopen:\n    \"\"\"Tests for reopen command.\"\"\"\n\n    def test_reopen(self):\n        \"\"\"Test reopen command.\"\"\"\n        arbiter = MockArbiter()\n        handlers = CommandHandlers(arbiter)\n\n        with patch('os.kill') as mock_kill:\n            result = handlers.reopen()\n\n        assert result[\"status\"] == \"reopening\"\n        mock_kill.assert_called_once_with(12345, signal.SIGUSR1)\n\n\nclass TestShutdown:\n    \"\"\"Tests for shutdown command.\"\"\"\n\n    def test_shutdown_graceful(self):\n        \"\"\"Test graceful shutdown.\"\"\"\n        arbiter = MockArbiter()\n        handlers = CommandHandlers(arbiter)\n\n        with patch('os.kill') as mock_kill:\n            result = handlers.shutdown()\n\n        assert result[\"status\"] == \"shutting_down\"\n        assert result[\"mode\"] == \"graceful\"\n        mock_kill.assert_called_once_with(12345, signal.SIGTERM)\n\n    def test_shutdown_quick(self):\n        \"\"\"Test quick shutdown.\"\"\"\n        arbiter = MockArbiter()\n        handlers = CommandHandlers(arbiter)\n\n        with patch('os.kill') as mock_kill:\n            result = handlers.shutdown(\"quick\")\n\n        assert result[\"status\"] == \"shutting_down\"\n        assert result[\"mode\"] == \"quick\"\n        mock_kill.assert_called_once_with(12345, signal.SIGINT)\n\n\nclass TestShowAll:\n    \"\"\"Tests for show all command.\"\"\"\n\n    def test_show_all_basic(self):\n        \"\"\"Test show all command.\"\"\"\n        arbiter = MockArbiter()\n        arbiter.WORKERS = {\n            1001: MockWorker(1001, 1),\n            1002: MockWorker(1002, 2),\n        }\n        handlers = CommandHandlers(arbiter)\n\n        result = handlers.show_all()\n\n        assert \"arbiter\" in result\n        assert result[\"arbiter\"][\"pid\"] == 12345\n        assert result[\"arbiter\"][\"type\"] == \"arbiter\"\n\n        assert \"web_workers\" in result\n        assert result[\"web_worker_count\"] == 2\n        assert len(result[\"web_workers\"]) == 2\n\n        assert \"dirty_arbiter\" in result\n        assert result[\"dirty_arbiter\"] is None\n\n        # No dirty workers when no dirty arbiter\n        assert result[\"dirty_worker_count\"] == 0\n\n    def test_show_all_with_dirty(self):\n        \"\"\"Test show all with dirty arbiter running.\"\"\"\n        arbiter = MockArbiter()\n        arbiter.dirty_arbiter_pid = 2000\n        handlers = CommandHandlers(arbiter)\n\n        result = handlers.show_all()\n\n        assert result[\"dirty_arbiter\"] is not None\n        assert result[\"dirty_arbiter\"][\"pid\"] == 2000\n        assert result[\"dirty_arbiter\"][\"type\"] == \"dirty_arbiter\"\n\n\nclass TestHelp:\n    \"\"\"Tests for help command.\"\"\"\n\n    def test_help(self):\n        \"\"\"Test help command.\"\"\"\n        arbiter = MockArbiter()\n        handlers = CommandHandlers(arbiter)\n\n        result = handlers.help()\n\n        assert \"commands\" in result\n        commands = result[\"commands\"]\n        assert \"show all\" in commands\n        assert \"show workers\" in commands\n        assert \"worker add [N]\" in commands\n        assert \"reload\" in commands\n        assert \"shutdown [graceful|quick]\" in commands\n"
  },
  {
    "path": "tests/ctl/test_protocol.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Tests for control socket protocol.\"\"\"\n\nimport json\nimport struct\nimport pytest\n\nfrom gunicorn.ctl.protocol import (\n    ControlProtocol,\n    ProtocolError,\n    make_request,\n    make_response,\n    make_error_response,\n)\n\n\nclass TestControlProtocolEncoding:\n    \"\"\"Tests for message encoding/decoding.\"\"\"\n\n    def test_encode_message_simple(self):\n        \"\"\"Test encoding a simple message.\"\"\"\n        data = {\"command\": \"test\"}\n        result = ControlProtocol.encode_message(data)\n\n        # First 4 bytes are length\n        length = struct.unpack('>I', result[:4])[0]\n        payload = result[4:]\n\n        assert length == len(payload)\n        assert json.loads(payload.decode('utf-8')) == data\n\n    def test_encode_message_unicode(self):\n        \"\"\"Test encoding message with unicode characters.\"\"\"\n        data = {\"message\": \"Hello \\u4e16\\u754c\"}\n        result = ControlProtocol.encode_message(data)\n\n        length = struct.unpack('>I', result[:4])[0]\n        payload = result[4:]\n\n        assert length == len(payload)\n        assert json.loads(payload.decode('utf-8')) == data\n\n    def test_decode_message_simple(self):\n        \"\"\"Test decoding a simple message.\"\"\"\n        data = {\"command\": \"test\", \"args\": [1, 2, 3]}\n        payload = json.dumps(data).encode('utf-8')\n        length = struct.pack('>I', len(payload))\n        raw = length + payload\n\n        result = ControlProtocol.decode_message(raw)\n        assert result == data\n\n    def test_decode_message_too_short(self):\n        \"\"\"Test decoding message that's too short.\"\"\"\n        with pytest.raises(ProtocolError) as exc_info:\n            ControlProtocol.decode_message(b'\\x00\\x00')\n        assert \"too short\" in str(exc_info.value)\n\n    def test_decode_message_incomplete(self):\n        \"\"\"Test decoding incomplete message.\"\"\"\n        # Length says 100 bytes but only 4 bytes provided\n        raw = struct.pack('>I', 100) + b'test'\n        with pytest.raises(ProtocolError) as exc_info:\n            ControlProtocol.decode_message(raw)\n        assert \"Incomplete\" in str(exc_info.value)\n\n    def test_roundtrip(self):\n        \"\"\"Test encode/decode roundtrip.\"\"\"\n        original = {\n            \"id\": 42,\n            \"command\": \"show workers\",\n            \"args\": [\"arg1\", 123, True, None],\n            \"nested\": {\"a\": 1, \"b\": [1, 2, 3]},\n        }\n\n        encoded = ControlProtocol.encode_message(original)\n        decoded = ControlProtocol.decode_message(encoded)\n\n        assert decoded == original\n\n\nclass TestMakeRequest:\n    \"\"\"Tests for request creation.\"\"\"\n\n    def test_make_request_simple(self):\n        \"\"\"Test creating a simple request.\"\"\"\n        result = make_request(1, \"show workers\")\n\n        assert result[\"id\"] == 1\n        assert result[\"command\"] == \"show workers\"\n        assert result[\"args\"] == []\n\n    def test_make_request_with_args(self):\n        \"\"\"Test creating a request with arguments.\"\"\"\n        result = make_request(42, \"worker add\", [2])\n\n        assert result[\"id\"] == 42\n        assert result[\"command\"] == \"worker add\"\n        assert result[\"args\"] == [2]\n\n\nclass TestMakeResponse:\n    \"\"\"Tests for response creation.\"\"\"\n\n    def test_make_response_simple(self):\n        \"\"\"Test creating a simple response.\"\"\"\n        result = make_response(1, {\"count\": 5})\n\n        assert result[\"id\"] == 1\n        assert result[\"status\"] == \"ok\"\n        assert result[\"data\"] == {\"count\": 5}\n\n    def test_make_response_empty_data(self):\n        \"\"\"Test creating response with no data.\"\"\"\n        result = make_response(1)\n\n        assert result[\"id\"] == 1\n        assert result[\"status\"] == \"ok\"\n        assert result[\"data\"] == {}\n\n\nclass TestMakeErrorResponse:\n    \"\"\"Tests for error response creation.\"\"\"\n\n    def test_make_error_response(self):\n        \"\"\"Test creating an error response.\"\"\"\n        result = make_error_response(1, \"Unknown command\")\n\n        assert result[\"id\"] == 1\n        assert result[\"status\"] == \"error\"\n        assert result[\"error\"] == \"Unknown command\"\n\n\nclass TestControlProtocolSocket:\n    \"\"\"Tests for socket reading/writing.\"\"\"\n\n    def test_read_write_message(self):\n        \"\"\"Test read/write through socket pair.\"\"\"\n        import socket\n        import threading\n\n        data = {\"id\": 1, \"command\": \"test\"}\n        received = []\n\n        # Create socket pair\n        server, client = socket.socketpair()\n\n        def reader():\n            received.append(ControlProtocol.read_message(server))\n\n        t = threading.Thread(target=reader)\n        t.start()\n\n        ControlProtocol.write_message(client, data)\n        t.join(timeout=2.0)\n\n        client.close()\n        server.close()\n\n        assert len(received) == 1\n        assert received[0] == data\n\n    def test_read_connection_closed(self):\n        \"\"\"Test reading from closed connection.\"\"\"\n        import socket\n\n        server, client = socket.socketpair()\n        client.close()\n\n        with pytest.raises(ConnectionError):\n            ControlProtocol.read_message(server)\n\n        server.close()\n\n    def test_read_message_too_large(self):\n        \"\"\"Test reading message exceeding max size.\"\"\"\n        import socket\n\n        server, client = socket.socketpair()\n\n        # Send a length that exceeds MAX_MESSAGE_SIZE\n        huge_length = ControlProtocol.MAX_MESSAGE_SIZE + 1\n        client.send(struct.pack('>I', huge_length))\n\n        with pytest.raises(ProtocolError) as exc_info:\n            ControlProtocol.read_message(server)\n        assert \"too large\" in str(exc_info.value)\n\n        client.close()\n        server.close()\n\n\nclass TestControlProtocolAsync:\n    \"\"\"Tests for async protocol methods.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_async_read_write(self):\n        \"\"\"Test async read/write using a unix server.\"\"\"\n        import asyncio\n        import tempfile\n        import os\n\n        data = {\"id\": 1, \"command\": \"async test\"}\n        received = []\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"test.sock\")\n\n            async def handler(reader, writer):\n                msg = await ControlProtocol.read_message_async(reader)\n                received.append(msg)\n                await ControlProtocol.write_message_async(writer, data)\n                writer.close()\n                await writer.wait_closed()\n\n            server = await asyncio.start_unix_server(handler, path=socket_path)\n\n            async with server:\n                reader, writer = await asyncio.open_unix_connection(socket_path)\n                await ControlProtocol.write_message_async(writer, data)\n                response = await ControlProtocol.read_message_async(reader)\n                writer.close()\n                await writer.wait_closed()\n\n            assert len(received) == 1\n            assert received[0] == data\n            assert response == data\n\n\nclass TestProtocolMaxSize:\n    \"\"\"Tests for protocol size limits.\"\"\"\n\n    def test_max_message_size_constant(self):\n        \"\"\"Test that MAX_MESSAGE_SIZE is set to a reasonable value.\"\"\"\n        # Should be 16 MB\n        assert ControlProtocol.MAX_MESSAGE_SIZE == 16 * 1024 * 1024\n\n    def test_encode_large_message(self):\n        \"\"\"Test encoding a large (but valid) message.\"\"\"\n        # Create a message with ~1MB of data\n        data = {\"data\": \"x\" * (1024 * 1024)}\n        encoded = ControlProtocol.encode_message(data)\n\n        # Should succeed and be decodable\n        decoded = ControlProtocol.decode_message(encoded)\n        assert decoded == data\n"
  },
  {
    "path": "tests/ctl/test_server.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Tests for control socket server.\"\"\"\n\nimport os\nimport tempfile\nimport time\nfrom unittest.mock import MagicMock\n\nimport pytest\n\nfrom gunicorn.ctl.server import ControlSocketServer\nfrom gunicorn.ctl.client import ControlClient\n\n\nclass MockWorker:\n    \"\"\"Mock worker for testing.\"\"\"\n\n    def __init__(self, pid, age, booted=True, aborted=False):\n        self.pid = pid\n        self.age = age\n        self.booted = booted\n        self.aborted = aborted\n        self.tmp = MagicMock()\n        self.tmp.last_update.return_value = time.monotonic()\n\n\nclass MockConfig:\n    \"\"\"Mock config for testing.\"\"\"\n\n    def __init__(self):\n        self.bind = ['127.0.0.1:8000']\n        self.workers = 4\n        self.worker_class = 'sync'\n        self.threads = 1\n        self.timeout = 30\n        self.graceful_timeout = 30\n        self.keepalive = 2\n        self.max_requests = 0\n        self.max_requests_jitter = 0\n        self.worker_connections = 1000\n        self.preload_app = False\n        self.daemon = False\n        self.pidfile = None\n        self.proc_name = 'test_app'\n        self.reload = False\n        self.dirty_workers = 0\n        self.dirty_apps = []\n        self.dirty_timeout = 30\n        self.control_socket = 'gunicorn.ctl'\n        self.control_socket_disable = False\n\n\nclass MockLog:\n    \"\"\"Mock logger for testing.\"\"\"\n\n    def debug(self, msg, *args):\n        pass\n\n    def info(self, msg, *args):\n        pass\n\n    def warning(self, msg, *args):\n        pass\n\n    def error(self, msg, *args):\n        pass\n\n    def exception(self, msg, *args):\n        pass\n\n\nclass MockArbiter:\n    \"\"\"Mock arbiter for testing.\"\"\"\n\n    def __init__(self):\n        self.cfg = MockConfig()\n        self.log = MockLog()\n        self.pid = 12345\n        self.WORKERS = {}\n        self.LISTENERS = []\n        self.dirty_arbiter_pid = 0\n        self.dirty_arbiter = None\n        self.num_workers = 4\n        self._stats = {\n            'start_time': time.time() - 3600,\n            'workers_spawned': 10,\n            'workers_killed': 5,\n            'reloads': 2,\n        }\n\n    def wakeup(self):\n        pass\n\n\nclass TestControlSocketServerInit:\n    \"\"\"Tests for server initialization.\"\"\"\n\n    def test_init(self):\n        \"\"\"Test server initialization.\"\"\"\n        arbiter = MockArbiter()\n        server = ControlSocketServer(arbiter, \"/tmp/test.sock\", 0o600)\n\n        assert server.arbiter is arbiter\n        assert server.socket_path == \"/tmp/test.sock\"\n        assert server.socket_mode == 0o600\n        assert server._running is False\n\n\nclass TestControlSocketServerLifecycle:\n    \"\"\"Tests for server start/stop.\"\"\"\n\n    def test_start_stop(self):\n        \"\"\"Test starting and stopping the server.\"\"\"\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"test.sock\")\n\n            arbiter = MockArbiter()\n            server = ControlSocketServer(arbiter, socket_path)\n\n            server.start()\n\n            # Wait for server to start\n            for _ in range(50):\n                if os.path.exists(socket_path):\n                    break\n                time.sleep(0.1)\n            time.sleep(0.2)  # Extra wait for server to be fully ready\n\n            assert os.path.exists(socket_path)\n\n            server.stop()\n\n            # Wait for cleanup\n            time.sleep(0.2)\n\n            # Socket should be cleaned up\n            assert not os.path.exists(socket_path)\n\n    def test_start_already_running(self):\n        \"\"\"Test that start is idempotent.\"\"\"\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"test.sock\")\n\n            arbiter = MockArbiter()\n            server = ControlSocketServer(arbiter, socket_path)\n\n            server.start()\n            first_thread = server._thread\n            server.start()\n\n            assert server._thread is first_thread\n\n            server.stop()\n\n    def test_stop_not_running(self):\n        \"\"\"Test stopping a non-running server.\"\"\"\n        arbiter = MockArbiter()\n        server = ControlSocketServer(arbiter, \"/tmp/test.sock\")\n\n        # Should not raise\n        server.stop()\n\n\nclass TestControlSocketServerIntegration:\n    \"\"\"Integration tests for server with client.\"\"\"\n\n    def test_show_workers(self):\n        \"\"\"Test show workers command.\"\"\"\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"test.sock\")\n\n            arbiter = MockArbiter()\n            arbiter.WORKERS = {\n                1001: MockWorker(1001, 1),\n                1002: MockWorker(1002, 2),\n            }\n            server = ControlSocketServer(arbiter, socket_path)\n\n            server.start()\n\n            # Wait for server to start\n            for _ in range(50):\n                if os.path.exists(socket_path):\n                    break\n                time.sleep(0.1)\n            time.sleep(0.2)  # Extra wait for server to be fully ready\n\n            try:\n                with ControlClient(socket_path, timeout=5.0) as client:\n                    result = client.send_command(\"show workers\")\n\n                assert result[\"count\"] == 2\n                assert len(result[\"workers\"]) == 2\n            finally:\n                server.stop()\n\n    def test_show_stats(self):\n        \"\"\"Test show stats command.\"\"\"\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"test.sock\")\n\n            arbiter = MockArbiter()\n            server = ControlSocketServer(arbiter, socket_path)\n\n            server.start()\n\n            for _ in range(50):\n                if os.path.exists(socket_path):\n                    break\n                time.sleep(0.1)\n            time.sleep(0.2)  # Extra wait for server to be fully ready\n\n            try:\n                with ControlClient(socket_path, timeout=5.0) as client:\n                    result = client.send_command(\"show stats\")\n\n                assert result[\"pid\"] == 12345\n                assert result[\"workers_spawned\"] == 10\n            finally:\n                server.stop()\n\n    def test_help_command(self):\n        \"\"\"Test help command.\"\"\"\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"test.sock\")\n\n            arbiter = MockArbiter()\n            server = ControlSocketServer(arbiter, socket_path)\n\n            server.start()\n\n            for _ in range(50):\n                if os.path.exists(socket_path):\n                    break\n                time.sleep(0.1)\n            time.sleep(0.2)  # Extra wait for server to be fully ready\n\n            try:\n                with ControlClient(socket_path, timeout=5.0) as client:\n                    result = client.send_command(\"help\")\n\n                assert \"commands\" in result\n                assert \"show workers\" in result[\"commands\"]\n            finally:\n                server.stop()\n\n    def test_worker_add(self):\n        \"\"\"Test worker add command.\"\"\"\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"test.sock\")\n\n            arbiter = MockArbiter()\n            arbiter.wakeup = MagicMock()\n            server = ControlSocketServer(arbiter, socket_path)\n\n            server.start()\n\n            for _ in range(50):\n                if os.path.exists(socket_path):\n                    break\n                time.sleep(0.1)\n            time.sleep(0.2)  # Extra wait for server to be fully ready\n\n            try:\n                with ControlClient(socket_path, timeout=5.0) as client:\n                    result = client.send_command(\"worker add 2\")\n\n                assert result[\"added\"] == 2\n                assert result[\"total\"] == 6\n                assert arbiter.num_workers == 6\n                arbiter.wakeup.assert_called()\n            finally:\n                server.stop()\n\n    def test_invalid_command(self):\n        \"\"\"Test handling invalid command.\"\"\"\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"test.sock\")\n\n            arbiter = MockArbiter()\n            server = ControlSocketServer(arbiter, socket_path)\n\n            server.start()\n\n            for _ in range(50):\n                if os.path.exists(socket_path):\n                    break\n                time.sleep(0.1)\n            time.sleep(0.2)  # Extra wait for server to be fully ready\n\n            try:\n                with ControlClient(socket_path, timeout=5.0) as client:\n                    with pytest.raises(Exception) as exc_info:\n                        client.send_command(\"invalid_command\")\n\n                    assert \"Unknown command\" in str(exc_info.value)\n            finally:\n                server.stop()\n\n    def test_multiple_commands(self):\n        \"\"\"Test sending multiple commands on same connection.\"\"\"\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"test.sock\")\n\n            arbiter = MockArbiter()\n            arbiter.WORKERS = {1001: MockWorker(1001, 1)}\n            server = ControlSocketServer(arbiter, socket_path)\n\n            server.start()\n\n            for _ in range(50):\n                if os.path.exists(socket_path):\n                    break\n                time.sleep(0.1)\n            time.sleep(0.2)  # Extra wait for server to be fully ready\n\n            try:\n                with ControlClient(socket_path, timeout=5.0) as client:\n                    result1 = client.send_command(\"show workers\")\n                    result2 = client.send_command(\"show stats\")\n                    result3 = client.send_command(\"help\")\n\n                assert result1[\"count\"] == 1\n                assert result2[\"pid\"] == 12345\n                assert \"commands\" in result3\n            finally:\n                server.stop()\n\n\nclass TestControlSocketServerPermissions:\n    \"\"\"Tests for socket permissions.\"\"\"\n\n    @pytest.mark.skipif(\n        os.uname().sysname == \"FreeBSD\",\n        reason=\"FreeBSD socket permissions behavior differs\"\n    )\n    def test_socket_permissions(self):\n        \"\"\"Test that socket is created with correct permissions.\"\"\"\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"test.sock\")\n\n            arbiter = MockArbiter()\n            server = ControlSocketServer(arbiter, socket_path, 0o660)\n\n            server.start()\n\n            # Wait for socket to exist\n            for _ in range(50):\n                if os.path.exists(socket_path):\n                    break\n                time.sleep(0.1)\n\n            # Extra wait for chmod to complete\n            time.sleep(0.2)\n\n            try:\n                mode = os.stat(socket_path).st_mode & 0o777\n                assert mode == 0o660\n            finally:\n                server.stop()\n"
  },
  {
    "path": "tests/dirty/__init__.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Tests for dirty worker streaming functionality.\"\"\"\n"
  },
  {
    "path": "tests/dirty/test_arbiter_signals.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Tests for dirty arbiter TTIN/TTOU signal handling.\"\"\"\n\nimport signal\nfrom unittest.mock import Mock\n\nimport pytest\n\n\nclass TestDirtyArbiterSignals:\n    \"\"\"Test TTIN/TTOU signal handling in DirtyArbiter.\"\"\"\n\n    @pytest.fixture\n    def arbiter(self, tmp_path):\n        \"\"\"Create a DirtyArbiter for testing.\"\"\"\n        from gunicorn.dirty.arbiter import DirtyArbiter\n\n        cfg = Mock()\n        cfg.dirty_workers = 2\n        cfg.dirty_apps = []\n        cfg.dirty_timeout = 30\n        cfg.dirty_graceful_timeout = 30\n        cfg.on_dirty_starting = Mock()\n        log = Mock()\n\n        arbiter = DirtyArbiter(cfg, log, socket_path=str(tmp_path / \"test.sock\"))\n        return arbiter\n\n    def test_initial_num_workers_from_config(self, arbiter):\n        \"\"\"num_workers should be initialized from config.\"\"\"\n        assert arbiter.num_workers == 2\n\n    def test_ttin_increases_num_workers(self, arbiter):\n        \"\"\"SIGTTIN should increase num_workers by 1.\"\"\"\n        assert arbiter.num_workers == 2\n        arbiter._signal_handler(signal.SIGTTIN, None)\n        assert arbiter.num_workers == 3\n\n    def test_ttin_logs_info(self, arbiter):\n        \"\"\"SIGTTIN should log info about the change.\"\"\"\n        arbiter._signal_handler(signal.SIGTTIN, None)\n        arbiter.log.info.assert_called()\n        call_args = arbiter.log.info.call_args[0]\n        assert \"SIGTTIN\" in call_args[0]\n        assert \"3\" in str(call_args)\n\n    def test_ttou_decreases_num_workers(self, arbiter):\n        \"\"\"SIGTTOU should decrease num_workers by 1.\"\"\"\n        arbiter.num_workers = 3\n        arbiter._signal_handler(signal.SIGTTOU, None)\n        assert arbiter.num_workers == 2\n\n    def test_ttou_logs_info(self, arbiter):\n        \"\"\"SIGTTOU should log info about the change.\"\"\"\n        arbiter.num_workers = 3\n        arbiter._signal_handler(signal.SIGTTOU, None)\n        arbiter.log.info.assert_called()\n        call_args = arbiter.log.info.call_args[0]\n        assert \"SIGTTOU\" in call_args[0]\n        assert \"2\" in str(call_args)\n\n    def test_ttou_respects_minimum_one_worker(self, arbiter):\n        \"\"\"SIGTTOU should not go below 1 worker by default.\"\"\"\n        arbiter.num_workers = 1\n        arbiter._signal_handler(signal.SIGTTOU, None)\n        assert arbiter.num_workers == 1\n\n    def test_ttou_logs_warning_at_minimum(self, arbiter):\n        \"\"\"SIGTTOU should log warning when at minimum.\"\"\"\n        arbiter.num_workers = 1\n        arbiter._signal_handler(signal.SIGTTOU, None)\n        arbiter.log.warning.assert_called()\n        call_args = arbiter.log.warning.call_args[0]\n        assert \"Cannot decrease below\" in call_args[0]\n\n    def test_ttou_respects_app_minimum(self, arbiter):\n        \"\"\"SIGTTOU should not go below app-required minimum.\"\"\"\n        # App requires 3 workers\n        arbiter.app_specs = {\n            'myapp:HeavyTask': {\n                'import_path': 'myapp:HeavyTask',\n                'worker_count': 3,\n                'original_spec': 'myapp:HeavyTask:3',\n            }\n        }\n        arbiter.num_workers = 3\n\n        # Should not decrease below 3\n        arbiter._signal_handler(signal.SIGTTOU, None)\n        assert arbiter.num_workers == 3\n        arbiter.log.warning.assert_called()\n\n    def test_ttou_with_unlimited_app(self, arbiter):\n        \"\"\"Apps with worker_count=None should not impose minimum.\"\"\"\n        arbiter.app_specs = {\n            'myapp:UnlimitedTask': {\n                'import_path': 'myapp:UnlimitedTask',\n                'worker_count': None,\n                'original_spec': 'myapp:UnlimitedTask',\n            }\n        }\n        arbiter.num_workers = 2\n\n        # Should decrease to 1 (default minimum)\n        arbiter._signal_handler(signal.SIGTTOU, None)\n        assert arbiter.num_workers == 1\n\n    def test_multiple_ttin_signals(self, arbiter):\n        \"\"\"Multiple TTIN signals should keep incrementing.\"\"\"\n        assert arbiter.num_workers == 2\n        arbiter._signal_handler(signal.SIGTTIN, None)\n        arbiter._signal_handler(signal.SIGTTIN, None)\n        arbiter._signal_handler(signal.SIGTTIN, None)\n        assert arbiter.num_workers == 5\n\n    def test_multiple_ttou_signals(self, arbiter):\n        \"\"\"Multiple TTOU signals should decrement until minimum.\"\"\"\n        arbiter.num_workers = 5\n        arbiter._signal_handler(signal.SIGTTOU, None)\n        arbiter._signal_handler(signal.SIGTTOU, None)\n        arbiter._signal_handler(signal.SIGTTOU, None)\n        arbiter._signal_handler(signal.SIGTTOU, None)\n        # Should stop at 1\n        assert arbiter.num_workers == 1\n\n\nclass TestGetMinimumWorkers:\n    \"\"\"Test _get_minimum_workers calculation.\"\"\"\n\n    @pytest.fixture\n    def arbiter(self, tmp_path):\n        \"\"\"Create a DirtyArbiter for testing.\"\"\"\n        from gunicorn.dirty.arbiter import DirtyArbiter\n\n        cfg = Mock()\n        cfg.dirty_workers = 2\n        cfg.dirty_apps = []\n        cfg.dirty_timeout = 30\n        cfg.dirty_graceful_timeout = 30\n        cfg.on_dirty_starting = Mock()\n        log = Mock()\n\n        arbiter = DirtyArbiter(cfg, log, socket_path=str(tmp_path / \"test.sock\"))\n        return arbiter\n\n    def test_minimum_workers_no_apps(self, arbiter):\n        \"\"\"With no apps, minimum should be 1.\"\"\"\n        arbiter.app_specs = {}\n        assert arbiter._get_minimum_workers() == 1\n\n    def test_minimum_workers_single_app_with_limit(self, arbiter):\n        \"\"\"Single app with worker_count should set minimum.\"\"\"\n        arbiter.app_specs = {\n            'app:Task': {\n                'import_path': 'app:Task',\n                'worker_count': 3,\n                'original_spec': 'app:Task:3',\n            }\n        }\n        assert arbiter._get_minimum_workers() == 3\n\n    def test_minimum_workers_single_app_unlimited(self, arbiter):\n        \"\"\"Single app with worker_count=None should use default minimum.\"\"\"\n        arbiter.app_specs = {\n            'app:Task': {\n                'import_path': 'app:Task',\n                'worker_count': None,\n                'original_spec': 'app:Task',\n            }\n        }\n        assert arbiter._get_minimum_workers() == 1\n\n    def test_minimum_workers_multiple_apps_with_limits(self, arbiter):\n        \"\"\"Multiple apps should use the maximum worker_count.\"\"\"\n        arbiter.app_specs = {\n            'app1:Task1': {\n                'import_path': 'app1:Task1',\n                'worker_count': 2,\n                'original_spec': 'app1:Task1:2',\n            },\n            'app2:Task2': {\n                'import_path': 'app2:Task2',\n                'worker_count': 4,\n                'original_spec': 'app2:Task2:4',\n            },\n            'app3:Task3': {\n                'import_path': 'app3:Task3',\n                'worker_count': 3,\n                'original_spec': 'app3:Task3:3',\n            },\n        }\n        # Maximum of (2, 4, 3) = 4\n        assert arbiter._get_minimum_workers() == 4\n\n    def test_minimum_workers_mixed_limited_and_unlimited(self, arbiter):\n        \"\"\"Mixed apps should use max of limited apps only.\"\"\"\n        arbiter.app_specs = {\n            'app1:Task1': {\n                'import_path': 'app1:Task1',\n                'worker_count': 2,\n                'original_spec': 'app1:Task1:2',\n            },\n            'app2:Task2': {\n                'import_path': 'app2:Task2',\n                'worker_count': None,\n                'original_spec': 'app2:Task2',\n            },\n            'app3:Task3': {\n                'import_path': 'app3:Task3',\n                'worker_count': 4,\n                'original_spec': 'app3:Task3:4',\n            },\n        }\n        # Maximum of (2, 4) = 4, None is ignored\n        assert arbiter._get_minimum_workers() == 4\n\n    def test_minimum_workers_all_unlimited(self, arbiter):\n        \"\"\"All unlimited apps should use default minimum.\"\"\"\n        arbiter.app_specs = {\n            'app1:Task1': {\n                'import_path': 'app1:Task1',\n                'worker_count': None,\n                'original_spec': 'app1:Task1',\n            },\n            'app2:Task2': {\n                'import_path': 'app2:Task2',\n                'worker_count': None,\n                'original_spec': 'app2:Task2',\n            },\n        }\n        assert arbiter._get_minimum_workers() == 1\n"
  },
  {
    "path": "tests/dirty/test_arbiter_streaming.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Tests for dirty arbiter streaming functionality.\"\"\"\n\nimport asyncio\nimport struct\nfrom unittest import mock\n\nimport pytest\n\nfrom gunicorn.dirty.protocol import (\n    DirtyProtocol,\n    BinaryProtocol,\n    make_request,\n    make_response,\n    make_chunk_message,\n    make_end_message,\n    make_error_response,\n    HEADER_SIZE,\n)\nfrom gunicorn.dirty.arbiter import DirtyArbiter\nfrom gunicorn.dirty.errors import DirtyError\n\n\nclass MockStreamWriter:\n    \"\"\"Mock StreamWriter that captures written messages.\"\"\"\n\n    def __init__(self):\n        self.messages = []\n        self._buffer = b\"\"\n        self.closed = False\n\n    def write(self, data):\n        self._buffer += data\n\n    async def drain(self):\n        # Decode the buffer to extract messages using binary protocol\n        while len(self._buffer) >= HEADER_SIZE:\n            # Decode header to get payload length\n            _, _, length = BinaryProtocol.decode_header(\n                self._buffer[:HEADER_SIZE]\n            )\n            total_size = HEADER_SIZE + length\n            if len(self._buffer) >= total_size:\n                msg_data = self._buffer[:total_size]\n                self._buffer = self._buffer[total_size:]\n                # decode_message returns (msg_type_str, request_id, payload_dict)\n                msg_type_str, request_id, payload_dict = BinaryProtocol.decode_message(msg_data)\n                # Reconstruct the dict format for backwards compatibility\n                result = {\"type\": msg_type_str, \"id\": request_id}\n                result.update(payload_dict)\n                self.messages.append(result)\n            else:\n                break\n\n    def close(self):\n        self.closed = True\n\n    async def wait_closed(self):\n        pass\n\n    def get_extra_info(self, name):\n        return None\n\n\nclass MockStreamReader:\n    \"\"\"Mock StreamReader that yields predefined messages.\"\"\"\n\n    def __init__(self, messages):\n        self._data = b''\n        for msg in messages:\n            self._data += BinaryProtocol._encode_from_dict(msg)\n        self._pos = 0\n\n    async def readexactly(self, n):\n        if self._pos + n > len(self._data):\n            raise asyncio.IncompleteReadError(self._data[self._pos:], n)\n        result = self._data[self._pos:self._pos + n]\n        self._pos += n\n        return result\n\n\ndef create_arbiter():\n    \"\"\"Create a test arbiter with mocked components.\"\"\"\n    cfg = mock.Mock()\n    cfg.dirty_timeout = 30\n    cfg.dirty_workers = 1\n    cfg.dirty_apps = []\n    cfg.dirty_graceful_timeout = 30\n    cfg.on_dirty_starting = mock.Mock()\n    cfg.dirty_post_fork = mock.Mock()\n    cfg.dirty_worker_exit = mock.Mock()\n\n    log = mock.Mock()\n\n    with mock.patch('tempfile.mkdtemp', return_value='/tmp/test-dirty'):\n        arbiter = DirtyArbiter(cfg, log)\n\n    arbiter.alive = True\n    arbiter.workers = {1234: mock.Mock()}  # Fake worker\n    arbiter.worker_sockets = {1234: '/tmp/worker.sock'}\n\n    return arbiter\n\n\nclass TestArbiterStreamingForwarding:\n    \"\"\"Tests for arbiter streaming message forwarding.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_forwards_chunk_messages(self):\n        \"\"\"Test that arbiter forwards chunk messages to client.\"\"\"\n        arbiter = create_arbiter()\n        client_writer = MockStreamWriter()\n\n        # Mock worker connection that returns chunks\n        chunk1 = make_chunk_message(123, \"Hello\")\n        chunk2 = make_chunk_message(123, \" World\")\n        end = make_end_message(123)\n\n        mock_reader = MockStreamReader([chunk1, chunk2, end])\n\n        async def mock_get_connection(pid):\n            return mock_reader, MockStreamWriter()\n\n        arbiter._get_worker_connection = mock_get_connection\n\n        request = make_request(123, \"test:App\", \"generate\")\n        await arbiter._execute_on_worker(1234, request, client_writer)\n\n        # Should have forwarded all messages\n        assert len(client_writer.messages) == 3\n        assert client_writer.messages[0][\"type\"] == \"chunk\"\n        assert client_writer.messages[0][\"data\"] == \"Hello\"\n        assert client_writer.messages[1][\"type\"] == \"chunk\"\n        assert client_writer.messages[1][\"data\"] == \" World\"\n        assert client_writer.messages[2][\"type\"] == \"end\"\n\n    @pytest.mark.asyncio\n    async def test_forwards_regular_response(self):\n        \"\"\"Test that arbiter forwards regular response to client.\"\"\"\n        arbiter = create_arbiter()\n        client_writer = MockStreamWriter()\n\n        response = make_response(123, {\"result\": 42})\n        mock_reader = MockStreamReader([response])\n\n        async def mock_get_connection(pid):\n            return mock_reader, MockStreamWriter()\n\n        arbiter._get_worker_connection = mock_get_connection\n\n        request = make_request(123, \"test:App\", \"compute\")\n        await arbiter._execute_on_worker(1234, request, client_writer)\n\n        assert len(client_writer.messages) == 1\n        assert client_writer.messages[0][\"type\"] == \"response\"\n        assert client_writer.messages[0][\"result\"] == {\"result\": 42}\n\n    @pytest.mark.asyncio\n    async def test_forwards_error_mid_stream(self):\n        \"\"\"Test that arbiter forwards error during streaming.\"\"\"\n        arbiter = create_arbiter()\n        client_writer = MockStreamWriter()\n\n        chunk = make_chunk_message(123, \"First\")\n        error = make_error_response(123, DirtyError(\"Something broke\"))\n\n        mock_reader = MockStreamReader([chunk, error])\n\n        async def mock_get_connection(pid):\n            return mock_reader, MockStreamWriter()\n\n        arbiter._get_worker_connection = mock_get_connection\n\n        request = make_request(123, \"test:App\", \"generate\")\n        await arbiter._execute_on_worker(1234, request, client_writer)\n\n        assert len(client_writer.messages) == 2\n        assert client_writer.messages[0][\"type\"] == \"chunk\"\n        assert client_writer.messages[1][\"type\"] == \"error\"\n\n    @pytest.mark.asyncio\n    async def test_timeout_during_streaming(self):\n        \"\"\"Test that timeout during streaming sends error.\"\"\"\n        arbiter = create_arbiter()\n        arbiter.cfg.dirty_timeout = 0.01  # Very short timeout\n        client_writer = MockStreamWriter()\n\n        # Reader that times out\n        class TimeoutReader:\n            async def readexactly(self, n):\n                await asyncio.sleep(1)  # Longer than timeout\n\n        async def mock_get_connection(pid):\n            return TimeoutReader(), MockStreamWriter()\n\n        arbiter._get_worker_connection = mock_get_connection\n\n        request = make_request(123, \"test:App\", \"generate\")\n        await arbiter._execute_on_worker(1234, request, client_writer)\n\n        assert len(client_writer.messages) == 1\n        assert client_writer.messages[0][\"type\"] == \"error\"\n        assert \"timeout\" in client_writer.messages[0][\"error\"][\"message\"].lower()\n\n\nclass TestArbiterRouteRequestStreaming:\n    \"\"\"Tests for route_request with streaming support.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_route_request_no_workers(self):\n        \"\"\"Test route_request when no workers available.\"\"\"\n        arbiter = create_arbiter()\n        arbiter.workers = {}  # No workers\n        client_writer = MockStreamWriter()\n\n        request = make_request(123, \"test:App\", \"generate\")\n        await arbiter.route_request(request, client_writer)\n\n        assert len(client_writer.messages) == 1\n        assert client_writer.messages[0][\"type\"] == \"error\"\n        assert \"No dirty workers\" in client_writer.messages[0][\"error\"][\"message\"]\n\n    @pytest.mark.asyncio\n    async def test_route_request_starts_consumer(self):\n        \"\"\"Test that route_request starts consumer if needed.\"\"\"\n        arbiter = create_arbiter()\n\n        # Mock _execute_on_worker to complete immediately\n        async def mock_execute(pid, request, client_writer):\n            response = make_response(123, \"result\")\n            await DirtyProtocol.write_message_async(client_writer, response)\n\n        arbiter._execute_on_worker = mock_execute\n\n        client_writer = MockStreamWriter()\n        request = make_request(123, \"test:App\", \"compute\")\n\n        # Worker queue should be created\n        assert 1234 not in arbiter.worker_queues\n\n        await arbiter.route_request(request, client_writer)\n\n        # Consumer should have been started\n        assert 1234 in arbiter.worker_queues\n        assert 1234 in arbiter.worker_consumers\n\n        # Clean up\n        arbiter.worker_consumers[1234].cancel()\n\n\nclass TestArbiterStreamingManyChunks:\n    \"\"\"Tests for streaming with many chunks.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_forwards_many_chunks(self):\n        \"\"\"Test that arbiter forwards many chunks correctly.\"\"\"\n        arbiter = create_arbiter()\n        client_writer = MockStreamWriter()\n\n        # Generate 50 chunks + end\n        messages = []\n        for i in range(50):\n            messages.append(make_chunk_message(123, f\"chunk-{i}\"))\n        messages.append(make_end_message(123))\n\n        mock_reader = MockStreamReader(messages)\n\n        async def mock_get_connection(pid):\n            return mock_reader, MockStreamWriter()\n\n        arbiter._get_worker_connection = mock_get_connection\n\n        request = make_request(123, \"test:App\", \"generate\")\n        await arbiter._execute_on_worker(1234, request, client_writer)\n\n        assert len(client_writer.messages) == 51\n        assert client_writer.messages[0][\"data\"] == \"chunk-0\"\n        assert client_writer.messages[49][\"data\"] == \"chunk-49\"\n        assert client_writer.messages[50][\"type\"] == \"end\"\n\n\nclass TestArbiterBackwardCompatibility:\n    \"\"\"Tests for backward compatibility with non-streaming.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_handles_regular_response(self):\n        \"\"\"Test that regular (non-streaming) responses still work.\"\"\"\n        arbiter = create_arbiter()\n        client_writer = MockStreamWriter()\n\n        response = make_response(123, [1, 2, 3, 4, 5])\n        mock_reader = MockStreamReader([response])\n\n        async def mock_get_connection(pid):\n            return mock_reader, MockStreamWriter()\n\n        arbiter._get_worker_connection = mock_get_connection\n\n        request = make_request(123, \"test:App\", \"get_list\")\n        await arbiter._execute_on_worker(1234, request, client_writer)\n\n        assert len(client_writer.messages) == 1\n        assert client_writer.messages[0][\"type\"] == \"response\"\n        assert client_writer.messages[0][\"result\"] == [1, 2, 3, 4, 5]\n\n    @pytest.mark.asyncio\n    async def test_handles_error_response(self):\n        \"\"\"Test that error responses still work.\"\"\"\n        arbiter = create_arbiter()\n        client_writer = MockStreamWriter()\n\n        error = make_error_response(123, DirtyError(\"Something failed\"))\n        mock_reader = MockStreamReader([error])\n\n        async def mock_get_connection(pid):\n            return mock_reader, MockStreamWriter()\n\n        arbiter._get_worker_connection = mock_get_connection\n\n        request = make_request(123, \"test:App\", \"fail\")\n        await arbiter._execute_on_worker(1234, request, client_writer)\n\n        assert len(client_writer.messages) == 1\n        assert client_writer.messages[0][\"type\"] == \"error\"\n"
  },
  {
    "path": "tests/dirty/test_client_streaming.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Tests for dirty client sync streaming functionality.\"\"\"\n\nimport socket\nimport struct\nimport pytest\nfrom unittest import mock\n\nfrom gunicorn.dirty.protocol import (\n    DirtyProtocol,\n    BinaryProtocol,\n    make_chunk_message,\n    make_end_message,\n    make_response,\n    make_error_response,\n    HEADER_SIZE,\n)\nfrom gunicorn.dirty.client import DirtyClient, DirtyStreamIterator\nfrom gunicorn.dirty.errors import DirtyError, DirtyConnectionError\n\n\nclass MockSocket:\n    \"\"\"Mock socket that returns predefined messages.\"\"\"\n\n    def __init__(self, messages):\n        self._data = b''\n        for msg in messages:\n            self._data += BinaryProtocol._encode_from_dict(msg)\n        self._pos = 0\n        self._sent = []\n        self.closed = False\n        self._timeout = None\n\n    def sendall(self, data):\n        self._sent.append(data)\n\n    def recv(self, n, flags=0):\n        if self._pos >= len(self._data):\n            return b''\n        end = min(self._pos + n, len(self._data))\n        result = self._data[self._pos:end]\n        self._pos = end\n        return result\n\n    def settimeout(self, timeout):\n        self._timeout = timeout\n\n    def close(self):\n        self.closed = True\n\n\ndef create_client_with_mock_socket(messages):\n    \"\"\"Create a client with a mock socket returning the given messages.\"\"\"\n    client = DirtyClient(\"/tmp/test.sock\")\n    client._sock = MockSocket(messages)\n    return client\n\n\nclass TestDirtyStreamIterator:\n    \"\"\"Tests for DirtyStreamIterator.\"\"\"\n\n    def test_stream_returns_iterator(self):\n        \"\"\"Test that stream() returns an iterator.\"\"\"\n        client = DirtyClient(\"/tmp/test.sock\")\n        result = client.stream(\"test:App\", \"generate\")\n        assert isinstance(result, DirtyStreamIterator)\n\n    def test_stream_iterator_yields_chunks(self):\n        \"\"\"Test that stream iterator yields chunks correctly.\"\"\"\n        messages = [\n            make_chunk_message(123, \"Hello\"),\n            make_chunk_message(123, \" \"),\n            make_chunk_message(123, \"World\"),\n            make_end_message(123),\n        ]\n        client = create_client_with_mock_socket(messages)\n\n        chunks = list(client.stream(\"test:App\", \"generate\"))\n\n        assert chunks == [\"Hello\", \" \", \"World\"]\n\n    def test_stream_iterator_yields_complex_chunks(self):\n        \"\"\"Test that stream iterator yields complex data types.\"\"\"\n        messages = [\n            make_chunk_message(123, {\"token\": \"Hello\", \"score\": 0.9}),\n            make_chunk_message(123, {\"token\": \"World\", \"score\": 0.8}),\n            make_end_message(123),\n        ]\n        client = create_client_with_mock_socket(messages)\n\n        chunks = list(client.stream(\"test:App\", \"generate\"))\n\n        assert len(chunks) == 2\n        assert chunks[0][\"token\"] == \"Hello\"\n        assert chunks[1][\"token\"] == \"World\"\n\n    def test_stream_iterator_handles_error(self):\n        \"\"\"Test that stream iterator raises on error message.\"\"\"\n        messages = [\n            make_chunk_message(123, \"First\"),\n            make_error_response(123, DirtyError(\"Something broke\")),\n        ]\n        client = create_client_with_mock_socket(messages)\n\n        iterator = client.stream(\"test:App\", \"generate\")\n\n        # First chunk should work\n        chunk = next(iterator)\n        assert chunk == \"First\"\n\n        # Second should raise error\n        with pytest.raises(DirtyError) as exc_info:\n            next(iterator)\n        assert \"Something broke\" in str(exc_info.value)\n\n    def test_stream_iterator_empty_stream(self):\n        \"\"\"Test that empty stream (just end) works.\"\"\"\n        messages = [make_end_message(123)]\n        client = create_client_with_mock_socket(messages)\n\n        chunks = list(client.stream(\"test:App\", \"generate\"))\n        assert chunks == []\n\n    def test_stream_iterator_stops_after_exhausted(self):\n        \"\"\"Test that iterator stays exhausted after StopIteration.\"\"\"\n        messages = [\n            make_chunk_message(123, \"Only\"),\n            make_end_message(123),\n        ]\n        client = create_client_with_mock_socket(messages)\n\n        iterator = client.stream(\"test:App\", \"generate\")\n\n        # Get the chunk\n        chunk = next(iterator)\n        assert chunk == \"Only\"\n\n        # Should stop\n        with pytest.raises(StopIteration):\n            next(iterator)\n\n        # Should stay stopped\n        with pytest.raises(StopIteration):\n            next(iterator)\n\n    def test_stream_iterator_with_for_loop(self):\n        \"\"\"Test stream iterator works in for loop.\"\"\"\n        messages = [\n            make_chunk_message(123, \"a\"),\n            make_chunk_message(123, \"b\"),\n            make_chunk_message(123, \"c\"),\n            make_end_message(123),\n        ]\n        client = create_client_with_mock_socket(messages)\n\n        result = \"\"\n        for chunk in client.stream(\"test:App\", \"generate\"):\n            result += chunk\n\n        assert result == \"abc\"\n\n    def test_stream_sends_request_on_first_iteration(self):\n        \"\"\"Test that request is sent on first next() call.\"\"\"\n        messages = [\n            make_chunk_message(123, \"data\"),\n            make_end_message(123),\n        ]\n        client = create_client_with_mock_socket(messages)\n\n        iterator = client.stream(\"test:App\", \"generate\", \"prompt_arg\")\n\n        # Before iteration, no request sent\n        assert len(client._sock._sent) == 0\n\n        # First iteration sends request\n        next(iterator)\n        assert len(client._sock._sent) == 1\n\n        # Decode sent request\n        sent_data = client._sock._sent[0]\n        _, _, length = BinaryProtocol.decode_header(sent_data[:HEADER_SIZE])\n        msg_type_str, request_id, payload = BinaryProtocol.decode_message(\n            sent_data[:HEADER_SIZE + length]\n        )\n\n        assert msg_type_str == \"request\"\n        assert payload[\"app_path\"] == \"test:App\"\n        assert payload[\"action\"] == \"generate\"\n        assert payload[\"args\"] == [\"prompt_arg\"]\n\n\nclass TestDirtyStreamIteratorEdgeCases:\n    \"\"\"Edge cases for streaming.\"\"\"\n\n    def test_stream_many_chunks(self):\n        \"\"\"Test streaming with many chunks.\"\"\"\n        messages = []\n        for i in range(100):\n            messages.append(make_chunk_message(123, f\"chunk-{i}\"))\n        messages.append(make_end_message(123))\n\n        client = create_client_with_mock_socket(messages)\n\n        chunks = list(client.stream(\"test:App\", \"generate\"))\n\n        assert len(chunks) == 100\n        assert chunks[0] == \"chunk-0\"\n        assert chunks[99] == \"chunk-99\"\n\n    def test_stream_with_kwargs(self):\n        \"\"\"Test streaming with keyword arguments.\"\"\"\n        messages = [\n            make_chunk_message(123, \"data\"),\n            make_end_message(123),\n        ]\n        client = create_client_with_mock_socket(messages)\n\n        # Use kwargs\n        list(client.stream(\"test:App\", \"generate\", \"arg1\", key=\"value\"))\n\n        # Check the sent request includes kwargs\n        sent_data = client._sock._sent[0]\n        _, _, length = BinaryProtocol.decode_header(sent_data[:HEADER_SIZE])\n        msg_type_str, request_id, payload = BinaryProtocol.decode_message(\n            sent_data[:HEADER_SIZE + length]\n        )\n\n        assert payload[\"args\"] == [\"arg1\"]\n        assert payload[\"kwargs\"] == {\"key\": \"value\"}\n"
  },
  {
    "path": "tests/dirty/test_client_streaming_async.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Tests for dirty client async streaming functionality.\"\"\"\n\nimport asyncio\nimport struct\nimport pytest\n\nfrom gunicorn.dirty.protocol import (\n    DirtyProtocol,\n    BinaryProtocol,\n    make_chunk_message,\n    make_end_message,\n    make_error_response,\n    HEADER_SIZE,\n)\nfrom gunicorn.dirty.client import DirtyClient, DirtyAsyncStreamIterator\nfrom gunicorn.dirty.errors import DirtyError, DirtyTimeoutError\n\n\nclass MockAsyncReader:\n    \"\"\"Mock async reader that returns predefined messages.\"\"\"\n\n    def __init__(self, messages):\n        self._data = b''\n        for msg in messages:\n            self._data += BinaryProtocol._encode_from_dict(msg)\n        self._pos = 0\n\n    async def readexactly(self, n):\n        if self._pos + n > len(self._data):\n            raise asyncio.IncompleteReadError(self._data[self._pos:], n)\n        result = self._data[self._pos:self._pos + n]\n        self._pos += n\n        return result\n\n\nclass MockAsyncWriter:\n    \"\"\"Mock async writer that captures sent data.\"\"\"\n\n    def __init__(self):\n        self._sent = []\n        self.closed = False\n\n    def write(self, data):\n        self._sent.append(data)\n\n    async def drain(self):\n        pass\n\n    def close(self):\n        self.closed = True\n\n    async def wait_closed(self):\n        pass\n\n\ndef create_async_client_with_mocks(messages):\n    \"\"\"Create a client with mock async reader/writer.\"\"\"\n    client = DirtyClient(\"/tmp/test.sock\")\n    client._reader = MockAsyncReader(messages)\n    client._writer = MockAsyncWriter()\n    return client\n\n\nclass TestDirtyAsyncStreamIterator:\n    \"\"\"Tests for DirtyAsyncStreamIterator.\"\"\"\n\n    def test_stream_async_returns_async_iterator(self):\n        \"\"\"Test that stream_async() returns an async iterator.\"\"\"\n        client = DirtyClient(\"/tmp/test.sock\")\n        result = client.stream_async(\"test:App\", \"generate\")\n        assert isinstance(result, DirtyAsyncStreamIterator)\n\n    @pytest.mark.asyncio\n    async def test_async_stream_yields_chunks(self):\n        \"\"\"Test that async stream iterator yields chunks correctly.\"\"\"\n        messages = [\n            make_chunk_message(123, \"Hello\"),\n            make_chunk_message(123, \" \"),\n            make_chunk_message(123, \"World\"),\n            make_end_message(123),\n        ]\n        client = create_async_client_with_mocks(messages)\n\n        chunks = []\n        async for chunk in client.stream_async(\"test:App\", \"generate\"):\n            chunks.append(chunk)\n\n        assert chunks == [\"Hello\", \" \", \"World\"]\n\n    @pytest.mark.asyncio\n    async def test_async_stream_yields_complex_chunks(self):\n        \"\"\"Test that async stream iterator yields complex data types.\"\"\"\n        messages = [\n            make_chunk_message(123, {\"token\": \"Hello\", \"score\": 0.9}),\n            make_chunk_message(123, {\"token\": \"World\", \"score\": 0.8}),\n            make_end_message(123),\n        ]\n        client = create_async_client_with_mocks(messages)\n\n        chunks = []\n        async for chunk in client.stream_async(\"test:App\", \"generate\"):\n            chunks.append(chunk)\n\n        assert len(chunks) == 2\n        assert chunks[0][\"token\"] == \"Hello\"\n        assert chunks[1][\"token\"] == \"World\"\n\n    @pytest.mark.asyncio\n    async def test_async_stream_handles_error(self):\n        \"\"\"Test that async stream iterator raises on error message.\"\"\"\n        messages = [\n            make_chunk_message(123, \"First\"),\n            make_error_response(123, DirtyError(\"Something broke\")),\n        ]\n        client = create_async_client_with_mocks(messages)\n\n        iterator = client.stream_async(\"test:App\", \"generate\")\n\n        # First chunk should work\n        chunk = await iterator.__anext__()\n        assert chunk == \"First\"\n\n        # Second should raise error\n        with pytest.raises(DirtyError) as exc_info:\n            await iterator.__anext__()\n        assert \"Something broke\" in str(exc_info.value)\n\n    @pytest.mark.asyncio\n    async def test_async_stream_empty_stream(self):\n        \"\"\"Test that empty stream (just end) works.\"\"\"\n        messages = [make_end_message(123)]\n        client = create_async_client_with_mocks(messages)\n\n        chunks = []\n        async for chunk in client.stream_async(\"test:App\", \"generate\"):\n            chunks.append(chunk)\n\n        assert chunks == []\n\n    @pytest.mark.asyncio\n    async def test_async_stream_stops_after_exhausted(self):\n        \"\"\"Test that async iterator stays exhausted after StopAsyncIteration.\"\"\"\n        messages = [\n            make_chunk_message(123, \"Only\"),\n            make_end_message(123),\n        ]\n        client = create_async_client_with_mocks(messages)\n\n        iterator = client.stream_async(\"test:App\", \"generate\")\n\n        # Get the chunk\n        chunk = await iterator.__anext__()\n        assert chunk == \"Only\"\n\n        # Should stop\n        with pytest.raises(StopAsyncIteration):\n            await iterator.__anext__()\n\n        # Should stay stopped\n        with pytest.raises(StopAsyncIteration):\n            await iterator.__anext__()\n\n    @pytest.mark.asyncio\n    async def test_async_stream_sends_request_on_first_iteration(self):\n        \"\"\"Test that request is sent on first async iteration.\"\"\"\n        messages = [\n            make_chunk_message(123, \"data\"),\n            make_end_message(123),\n        ]\n        client = create_async_client_with_mocks(messages)\n\n        iterator = client.stream_async(\"test:App\", \"generate\", \"prompt_arg\")\n\n        # Before iteration, no request sent\n        assert len(client._writer._sent) == 0\n\n        # First iteration sends request\n        await iterator.__anext__()\n        assert len(client._writer._sent) == 1\n\n        # Decode sent request\n        sent_data = client._writer._sent[0]\n        _, _, length = BinaryProtocol.decode_header(sent_data[:HEADER_SIZE])\n        msg_type_str, request_id, payload = BinaryProtocol.decode_message(\n            sent_data[:HEADER_SIZE + length]\n        )\n\n        assert msg_type_str == \"request\"\n        assert payload[\"app_path\"] == \"test:App\"\n        assert payload[\"action\"] == \"generate\"\n        assert payload[\"args\"] == [\"prompt_arg\"]\n\n\nclass TestDirtyAsyncStreamIteratorEdgeCases:\n    \"\"\"Edge cases for async streaming.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_async_stream_many_chunks(self):\n        \"\"\"Test async streaming with many chunks.\"\"\"\n        messages = []\n        for i in range(100):\n            messages.append(make_chunk_message(123, f\"chunk-{i}\"))\n        messages.append(make_end_message(123))\n\n        client = create_async_client_with_mocks(messages)\n\n        chunks = []\n        async for chunk in client.stream_async(\"test:App\", \"generate\"):\n            chunks.append(chunk)\n\n        assert len(chunks) == 100\n        assert chunks[0] == \"chunk-0\"\n        assert chunks[99] == \"chunk-99\"\n\n    @pytest.mark.asyncio\n    async def test_async_stream_with_kwargs(self):\n        \"\"\"Test async streaming with keyword arguments.\"\"\"\n        messages = [\n            make_chunk_message(123, \"data\"),\n            make_end_message(123),\n        ]\n        client = create_async_client_with_mocks(messages)\n\n        # Use kwargs\n        chunks = []\n        async for chunk in client.stream_async(\"test:App\", \"generate\", \"arg1\", key=\"value\"):\n            chunks.append(chunk)\n\n        # Check the sent request includes kwargs\n        sent_data = client._writer._sent[0]\n        _, _, length = BinaryProtocol.decode_header(sent_data[:HEADER_SIZE])\n        msg_type_str, request_id, payload = BinaryProtocol.decode_message(\n            sent_data[:HEADER_SIZE + length]\n        )\n\n        assert payload[\"args\"] == [\"arg1\"]\n        assert payload[\"kwargs\"] == {\"key\": \"value\"}\n\n\nclass TestDirtyAsyncStreamTimeout:\n    \"\"\"Tests for async streaming timeout handling.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_async_stream_timeout(self):\n        \"\"\"Test that timeout during async streaming raises DirtyTimeoutError.\"\"\"\n        client = DirtyClient(\"/tmp/test.sock\", timeout=0.01)\n\n        # Create a reader that times out\n        class SlowReader:\n            async def readexactly(self, n):\n                await asyncio.sleep(1)  # Longer than timeout\n\n        client._reader = SlowReader()\n        client._writer = MockAsyncWriter()\n\n        iterator = client.stream_async(\"test:App\", \"generate\")\n\n        with pytest.raises(DirtyTimeoutError):\n            await iterator.__anext__()\n"
  },
  {
    "path": "tests/dirty/test_multi_app_routing.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Tests for routing requests to multiple dirty apps.\n\nThis module verifies that when multiple dirty apps are configured,\nmessages are correctly routed to the appropriate app based on app_path.\n\"\"\"\n\nimport asyncio\nimport os\nimport struct\nimport tempfile\nimport pytest\n\nfrom concurrent.futures import ThreadPoolExecutor\n\nfrom gunicorn.config import Config\nfrom gunicorn.dirty.worker import DirtyWorker\nfrom gunicorn.dirty.arbiter import DirtyArbiter\nfrom gunicorn.dirty.protocol import (\n    DirtyProtocol,\n    BinaryProtocol,\n    make_request,\n    HEADER_SIZE,\n)\nfrom gunicorn.dirty.errors import DirtyAppNotFoundError\n\n\n# App paths for test apps\nCOUNTER_APP_PATH = \"tests.support_dirty_apps:CounterApp\"\nECHO_APP_PATH = \"tests.support_dirty_apps:EchoApp\"\n\n\nclass MockLog:\n    \"\"\"Mock logger for testing.\"\"\"\n\n    def __init__(self):\n        self.messages = []\n\n    def debug(self, msg, *args):\n        self.messages.append((\"debug\", msg % args if args else msg))\n\n    def info(self, msg, *args):\n        self.messages.append((\"info\", msg % args if args else msg))\n\n    def warning(self, msg, *args):\n        self.messages.append((\"warning\", msg % args if args else msg))\n\n    def error(self, msg, *args):\n        self.messages.append((\"error\", msg % args if args else msg))\n\n    def critical(self, msg, *args):\n        self.messages.append((\"critical\", msg % args if args else msg))\n\n    def exception(self, msg, *args):\n        self.messages.append((\"exception\", msg % args if args else msg))\n\n    def close_on_exec(self):\n        pass\n\n    def reopen_files(self):\n        pass\n\n\nclass MockStreamWriter:\n    \"\"\"Mock StreamWriter that captures written messages.\"\"\"\n\n    def __init__(self):\n        self.messages = []\n        self._buffer = b\"\"\n        self.closed = False\n\n    def write(self, data):\n        self._buffer += data\n\n    async def drain(self):\n        # Decode the buffer to extract messages using binary protocol\n        while len(self._buffer) >= HEADER_SIZE:\n            # Decode header to get payload length\n            _, _, length = BinaryProtocol.decode_header(\n                self._buffer[:HEADER_SIZE]\n            )\n            total_size = HEADER_SIZE + length\n            if len(self._buffer) >= total_size:\n                msg_data = self._buffer[:total_size]\n                self._buffer = self._buffer[total_size:]\n                # decode_message returns (msg_type_str, request_id, payload_dict)\n                msg_type_str, request_id, payload_dict = BinaryProtocol.decode_message(msg_data)\n                # Reconstruct the dict format for backwards compatibility\n                result = {\"type\": msg_type_str, \"id\": request_id}\n                result.update(payload_dict)\n                self.messages.append(result)\n            else:\n                break\n\n    def close(self):\n        self.closed = True\n\n    async def wait_closed(self):\n        pass\n\n    def get_extra_info(self, name):\n        return None\n\n\nclass TestWorkerMultiAppLoading:\n    \"\"\"Tests for loading multiple apps in a worker.\"\"\"\n\n    def test_worker_loads_multiple_apps(self):\n        \"\"\"Test that worker loads all configured apps.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[COUNTER_APP_PATH, ECHO_APP_PATH],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            worker.load_apps()\n\n            # Both apps should be loaded\n            assert COUNTER_APP_PATH in worker.apps\n            assert ECHO_APP_PATH in worker.apps\n\n            # Apps should be initialized\n            counter_app = worker.apps[COUNTER_APP_PATH]\n            echo_app = worker.apps[ECHO_APP_PATH]\n            assert counter_app.initialized is True\n            assert echo_app.initialized is True\n\n            worker._cleanup()\n\n    def test_worker_apps_are_distinct_instances(self):\n        \"\"\"Test that each app is a distinct instance.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[COUNTER_APP_PATH, ECHO_APP_PATH],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            worker.load_apps()\n\n            counter_app = worker.apps[COUNTER_APP_PATH]\n            echo_app = worker.apps[ECHO_APP_PATH]\n\n            # They should be different instances\n            assert counter_app is not echo_app\n\n            # They should be different types\n            assert type(counter_app).__name__ == \"CounterApp\"\n            assert type(echo_app).__name__ == \"EchoApp\"\n\n            worker._cleanup()\n\n\nclass TestWorkerMultiAppRouting:\n    \"\"\"Tests for routing requests to correct app based on app_path.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_worker_routes_to_counter_app(self):\n        \"\"\"Test that worker routes request to CounterApp correctly.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[COUNTER_APP_PATH, ECHO_APP_PATH],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            worker.load_apps()\n            worker._executor = ThreadPoolExecutor(max_workers=1)\n\n            try:\n                # Call increment on CounterApp\n                result = await worker.execute(\n                    COUNTER_APP_PATH, \"increment\", [], {\"amount\": 5}\n                )\n                assert result == 5\n\n                # Call get_value on CounterApp\n                result = await worker.execute(\n                    COUNTER_APP_PATH, \"get_value\", [], {}\n                )\n                assert result == 5\n            finally:\n                worker._cleanup()\n\n    @pytest.mark.asyncio\n    async def test_worker_routes_to_echo_app(self):\n        \"\"\"Test that worker routes request to EchoApp correctly.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[COUNTER_APP_PATH, ECHO_APP_PATH],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            worker.load_apps()\n            worker._executor = ThreadPoolExecutor(max_workers=1)\n\n            try:\n                # Call echo on EchoApp\n                result = await worker.execute(\n                    ECHO_APP_PATH, \"echo\", [\"hello\"], {}\n                )\n                assert result == \"ECHO: hello\"\n\n                # Set new prefix\n                result = await worker.execute(\n                    ECHO_APP_PATH, \"set_prefix\", [\"TEST>\"], {}\n                )\n                assert result == \"TEST>\"\n\n                # Echo with new prefix\n                result = await worker.execute(\n                    ECHO_APP_PATH, \"echo\", [\"world\"], {}\n                )\n                assert result == \"TEST> world\"\n            finally:\n                worker._cleanup()\n\n    @pytest.mark.asyncio\n    async def test_worker_routes_mixed_requests(self):\n        \"\"\"Test routing interleaved requests to different apps.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[COUNTER_APP_PATH, ECHO_APP_PATH],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            worker.load_apps()\n            worker._executor = ThreadPoolExecutor(max_workers=1)\n\n            try:\n                # Interleave calls to both apps\n                result = await worker.execute(\n                    COUNTER_APP_PATH, \"increment\", [1], {}\n                )\n                assert result == 1\n\n                result = await worker.execute(\n                    ECHO_APP_PATH, \"echo\", [\"first\"], {}\n                )\n                assert result == \"ECHO: first\"\n\n                result = await worker.execute(\n                    COUNTER_APP_PATH, \"increment\", [2], {}\n                )\n                assert result == 3\n\n                result = await worker.execute(\n                    ECHO_APP_PATH, \"echo\", [\"second\"], {}\n                )\n                assert result == \"ECHO: second\"\n\n                # Verify final state of each app\n                result = await worker.execute(\n                    COUNTER_APP_PATH, \"get_value\", [], {}\n                )\n                assert result == 3\n\n                result = await worker.execute(\n                    ECHO_APP_PATH, \"get_echo_count\", [], {}\n                )\n                assert result == 2\n            finally:\n                worker._cleanup()\n\n\nclass TestAppStateSeparation:\n    \"\"\"Tests for verifying apps maintain independent state.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_apps_maintain_separate_state(self):\n        \"\"\"Test that multiple apps maintain independent state.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[COUNTER_APP_PATH, ECHO_APP_PATH],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            worker.load_apps()\n            worker._executor = ThreadPoolExecutor(max_workers=1)\n\n            try:\n                # Modify CounterApp state\n                await worker.execute(COUNTER_APP_PATH, \"increment\", [10], {})\n                await worker.execute(COUNTER_APP_PATH, \"increment\", [5], {})\n\n                # Modify EchoApp state\n                await worker.execute(ECHO_APP_PATH, \"set_prefix\", [\"CUSTOM:\"], {})\n                await worker.execute(ECHO_APP_PATH, \"echo\", [\"msg1\"], {})\n                await worker.execute(ECHO_APP_PATH, \"echo\", [\"msg2\"], {})\n\n                # Verify CounterApp state is independent\n                counter_val = await worker.execute(\n                    COUNTER_APP_PATH, \"get_value\", [], {}\n                )\n                assert counter_val == 15\n\n                # Verify EchoApp state is independent\n                prefix = await worker.execute(\n                    ECHO_APP_PATH, \"get_prefix\", [], {}\n                )\n                assert prefix == \"CUSTOM:\"\n\n                echo_count = await worker.execute(\n                    ECHO_APP_PATH, \"get_echo_count\", [], {}\n                )\n                assert echo_count == 2\n\n                # Reset CounterApp and verify EchoApp unaffected\n                await worker.execute(COUNTER_APP_PATH, \"reset\", [], {})\n\n                counter_val = await worker.execute(\n                    COUNTER_APP_PATH, \"get_value\", [], {}\n                )\n                assert counter_val == 0\n\n                # EchoApp should be unaffected\n                echo_count = await worker.execute(\n                    ECHO_APP_PATH, \"get_echo_count\", [], {}\n                )\n                assert echo_count == 2\n            finally:\n                worker._cleanup()\n\n\nclass TestUnknownAppPath:\n    \"\"\"Tests for handling unknown app paths.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_unknown_app_path_raises_error(self):\n        \"\"\"Test that unknown app_path raises DirtyAppNotFoundError.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[COUNTER_APP_PATH, ECHO_APP_PATH],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            worker.load_apps()\n            worker._executor = ThreadPoolExecutor(max_workers=1)\n\n            try:\n                with pytest.raises(DirtyAppNotFoundError):\n                    await worker.execute(\n                        \"nonexistent:App\", \"action\", [], {}\n                    )\n            finally:\n                worker._cleanup()\n\n    @pytest.mark.asyncio\n    async def test_handle_request_unknown_app_returns_error(self):\n        \"\"\"Test that handle_request returns error for unknown app.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[COUNTER_APP_PATH],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            worker.load_apps()\n            worker._executor = ThreadPoolExecutor(max_workers=1)\n\n            try:\n                request = make_request(\n                    request_id=\"test-unknown\",\n                    app_path=\"unknown:App\",\n                    action=\"test\"\n                )\n\n                writer = MockStreamWriter()\n                await worker.handle_request(request, writer)\n\n                assert len(writer.messages) == 1\n                response = writer.messages[0]\n                assert response[\"type\"] == DirtyProtocol.MSG_TYPE_ERROR\n                assert \"unknown:App\" in response[\"error\"][\"message\"]\n            finally:\n                worker._cleanup()\n\n\nclass TestConcurrentMultiAppRequests:\n    \"\"\"Tests for concurrent requests to different apps.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_concurrent_requests_to_different_apps(self):\n        \"\"\"Test concurrent requests routed to different apps.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_threads\", 4)  # Allow concurrent execution\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[COUNTER_APP_PATH, ECHO_APP_PATH],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            worker.load_apps()\n            worker._executor = ThreadPoolExecutor(max_workers=4)\n\n            try:\n                # Create concurrent tasks for both apps\n                tasks = [\n                    worker.execute(COUNTER_APP_PATH, \"increment\", [1], {}),\n                    worker.execute(ECHO_APP_PATH, \"echo\", [\"msg1\"], {}),\n                    worker.execute(COUNTER_APP_PATH, \"increment\", [2], {}),\n                    worker.execute(ECHO_APP_PATH, \"echo\", [\"msg2\"], {}),\n                    worker.execute(COUNTER_APP_PATH, \"increment\", [3], {}),\n                    worker.execute(ECHO_APP_PATH, \"echo\", [\"msg3\"], {}),\n                ]\n\n                results = await asyncio.gather(*tasks)\n\n                # Verify echo results are correct (regardless of order)\n                echo_results = [r for r in results if isinstance(r, str)]\n                assert len(echo_results) == 3\n                assert all(r.startswith(\"ECHO:\") for r in echo_results)\n\n                # Counter results will vary based on execution order\n                # but final state should reflect all increments\n                counter_val = await worker.execute(\n                    COUNTER_APP_PATH, \"get_value\", [], {}\n                )\n                assert counter_val == 6  # 1 + 2 + 3\n            finally:\n                worker._cleanup()\n\n\nclass TestMultiAppProtocolHandling:\n    \"\"\"Tests for protocol-level handling of multi-app requests.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_handle_request_routes_correctly(self):\n        \"\"\"Test handle_request routes to correct app via protocol.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[COUNTER_APP_PATH, ECHO_APP_PATH],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            worker.load_apps()\n            worker._executor = ThreadPoolExecutor(max_workers=1)\n\n            try:\n                # Request to CounterApp\n                request1 = make_request(\n                    request_id=\"req-counter\",\n                    app_path=COUNTER_APP_PATH,\n                    action=\"increment\",\n                    args=[5]\n                )\n                writer1 = MockStreamWriter()\n                await worker.handle_request(request1, writer1)\n\n                assert len(writer1.messages) == 1\n                assert writer1.messages[0][\"type\"] == DirtyProtocol.MSG_TYPE_RESPONSE\n                assert writer1.messages[0][\"result\"] == 5\n\n                # Request to EchoApp\n                request2 = make_request(\n                    request_id=\"req-echo\",\n                    app_path=ECHO_APP_PATH,\n                    action=\"echo\",\n                    args=[\"test message\"]\n                )\n                writer2 = MockStreamWriter()\n                await worker.handle_request(request2, writer2)\n\n                assert len(writer2.messages) == 1\n                assert writer2.messages[0][\"type\"] == DirtyProtocol.MSG_TYPE_RESPONSE\n                assert writer2.messages[0][\"result\"] == \"ECHO: test message\"\n            finally:\n                worker._cleanup()\n\n\nclass TestMultiAppCleanup:\n    \"\"\"Tests for cleanup of multiple apps.\"\"\"\n\n    def test_cleanup_closes_all_apps(self):\n        \"\"\"Test that cleanup closes all loaded apps.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[COUNTER_APP_PATH, ECHO_APP_PATH],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            worker.load_apps()\n\n            counter_app = worker.apps[COUNTER_APP_PATH]\n            echo_app = worker.apps[ECHO_APP_PATH]\n\n            assert counter_app.closed is False\n            assert echo_app.closed is False\n\n            worker._cleanup()\n\n            assert counter_app.closed is True\n            assert echo_app.closed is True\n\n\nclass TestMultiAppArbiterIntegration:\n    \"\"\"Tests for arbiter routing with multiple apps configured.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_arbiter_routes_no_workers_error(self):\n        \"\"\"Test arbiter returns error when no workers for multi-app config.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_workers\", 0)\n        cfg.set(\"dirty_apps\", [COUNTER_APP_PATH, ECHO_APP_PATH])\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.pid = os.getpid()\n\n        try:\n            # Request to CounterApp - should fail (no workers)\n            request = make_request(\n                request_id=\"test-counter\",\n                app_path=COUNTER_APP_PATH,\n                action=\"increment\"\n            )\n\n            writer = MockStreamWriter()\n            await arbiter.route_request(request, writer)\n\n            assert len(writer.messages) == 1\n            response = writer.messages[0]\n            assert response[\"type\"] == DirtyProtocol.MSG_TYPE_ERROR\n            assert \"No dirty workers available\" in response[\"error\"][\"message\"]\n        finally:\n            arbiter._cleanup_sync()\n\n    def test_arbiter_config_has_multiple_apps(self):\n        \"\"\"Test arbiter config correctly stores multiple apps.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_apps\", [COUNTER_APP_PATH, ECHO_APP_PATH])\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n\n        try:\n            app_paths = arbiter.cfg.dirty_apps\n            assert COUNTER_APP_PATH in app_paths\n            assert ECHO_APP_PATH in app_paths\n            assert len(app_paths) == 2\n        finally:\n            arbiter._cleanup_sync()\n"
  },
  {
    "path": "tests/dirty/test_per_app_worker_allocation.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Integration tests for per-app worker allocation.\"\"\"\n\nimport pytest\n\nfrom gunicorn.config import Config\nfrom gunicorn.dirty.arbiter import DirtyArbiter\n\n\nclass MockLog:\n    \"\"\"Mock logger for testing.\"\"\"\n\n    def __init__(self):\n        self.messages = []\n\n    def debug(self, msg, *args):\n        self.messages.append((\"debug\", msg % args if args else msg))\n\n    def info(self, msg, *args):\n        self.messages.append((\"info\", msg % args if args else msg))\n\n    def warning(self, msg, *args):\n        self.messages.append((\"warning\", msg % args if args else msg))\n\n    def error(self, msg, *args):\n        self.messages.append((\"error\", msg % args if args else msg))\n\n    def critical(self, msg, *args):\n        self.messages.append((\"critical\", msg % args if args else msg))\n\n    def exception(self, msg, *args):\n        self.messages.append((\"exception\", msg % args if args else msg))\n\n    def close_on_exec(self):\n        pass\n\n    def reopen_files(self):\n        pass\n\n\nclass TestPerAppWorkerAllocation:\n    \"\"\"Integration tests for per-app worker allocation.\"\"\"\n\n    def test_heavy_app_loaded_on_limited_workers(self):\n        \"\"\"App with workers=2 only loaded on 2 of 4 workers.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_workers\", 4)\n        cfg.set(\"dirty_apps\", [\n            \"tests.support_dirty_app:TestDirtyApp\",      # unlimited\n            \"tests.support_dirty_app:SlowDirtyApp:2\",    # limited to 2\n        ])\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n\n        # Simulate spawning 4 workers\n        for i in range(4):\n            apps = arbiter._get_apps_for_new_worker()\n            arbiter._register_worker_apps(1000 + i, apps)\n\n        # Check distribution\n        unlimited_app = \"tests.support_dirty_app:TestDirtyApp\"\n        limited_app = \"tests.support_dirty_app:SlowDirtyApp\"\n\n        # Unlimited app should be on all 4 workers\n        assert len(arbiter.app_worker_map[unlimited_app]) == 4\n\n        # Limited app should only be on 2 workers\n        assert len(arbiter.app_worker_map[limited_app]) == 2\n\n        arbiter._cleanup_sync()\n\n    def test_light_app_loaded_on_all_workers(self):\n        \"\"\"App with workers=None loaded on all workers.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_workers\", 4)\n        cfg.set(\"dirty_apps\", [\n            \"tests.support_dirty_app:TestDirtyApp\",\n        ])\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n\n        # Simulate spawning 4 workers\n        for i in range(4):\n            apps = arbiter._get_apps_for_new_worker()\n            arbiter._register_worker_apps(1000 + i, apps)\n\n        # App should be on all 4 workers\n        app_path = \"tests.support_dirty_app:TestDirtyApp\"\n        assert len(arbiter.app_worker_map[app_path]) == 4\n\n        arbiter._cleanup_sync()\n\n    def test_mixed_apps_correct_distribution(self):\n        \"\"\"Mix of limited and unlimited apps distributed correctly.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_workers\", 4)\n        cfg.set(\"dirty_apps\", [\n            \"tests.support_dirty_app:TestDirtyApp\",      # unlimited\n            \"tests.support_dirty_app:SlowDirtyApp:1\",    # limited to 1\n        ])\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n\n        # Simulate spawning 4 workers\n        for i in range(4):\n            apps = arbiter._get_apps_for_new_worker()\n            arbiter._register_worker_apps(1000 + i, apps)\n\n        unlimited_app = \"tests.support_dirty_app:TestDirtyApp\"\n        limited_app = \"tests.support_dirty_app:SlowDirtyApp\"\n\n        # Unlimited app on all workers\n        assert len(arbiter.app_worker_map[unlimited_app]) == 4\n\n        # Limited app on only 1 worker\n        assert len(arbiter.app_worker_map[limited_app]) == 1\n\n        arbiter._cleanup_sync()\n\n    @pytest.mark.asyncio\n    async def test_request_routing_respects_allocation(self):\n        \"\"\"Requests only routed to workers with the target app.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_apps\", [\n            \"tests.support_dirty_app:TestDirtyApp\",\n            \"tests.support_dirty_app:SlowDirtyApp:1\",\n        ])\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n\n        # Set up workers\n        arbiter.workers[1001] = \"worker1\"\n        arbiter.workers[1002] = \"worker2\"\n\n        # Worker 1001 has both apps, worker 1002 has only TestDirtyApp\n        arbiter._register_worker_apps(1001, [\n            \"tests.support_dirty_app:TestDirtyApp\",\n            \"tests.support_dirty_app:SlowDirtyApp\",\n        ])\n        arbiter._register_worker_apps(1002, [\n            \"tests.support_dirty_app:TestDirtyApp\",\n        ])\n\n        # Request for SlowDirtyApp should only go to worker 1001\n        worker = await arbiter._get_available_worker(\"tests.support_dirty_app:SlowDirtyApp\")\n        assert worker == 1001\n\n        # Request for TestDirtyApp should go to either\n        worker = await arbiter._get_available_worker(\"tests.support_dirty_app:TestDirtyApp\")\n        assert worker in [1001, 1002]\n\n        arbiter._cleanup_sync()\n\n    def test_worker_crash_app_reassigned_to_new_worker(self):\n        \"\"\"When worker dies, new worker gets the app it had.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_apps\", [\n            \"tests.support_dirty_app:TestDirtyApp\",\n            \"tests.support_dirty_app:SlowDirtyApp:1\",\n        ])\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.pid = 12345\n\n        # Set up initial workers\n        arbiter.workers[1001] = \"worker1\"\n        arbiter.worker_sockets[1001] = \"/tmp/fake1.sock\"\n\n        # Worker 1001 has both apps\n        arbiter._register_worker_apps(1001, [\n            \"tests.support_dirty_app:TestDirtyApp\",\n            \"tests.support_dirty_app:SlowDirtyApp\",\n        ])\n\n        # Simulate worker crash\n        arbiter._cleanup_worker(1001)\n\n        # Apps should be queued for respawn\n        assert len(arbiter._pending_respawns) == 1\n        pending_apps = arbiter._pending_respawns[0]\n        assert \"tests.support_dirty_app:TestDirtyApp\" in pending_apps\n        assert \"tests.support_dirty_app:SlowDirtyApp\" in pending_apps\n\n        arbiter._cleanup_sync()\n\n    @pytest.mark.asyncio\n    async def test_worker_crash_other_workers_still_serve_app(self):\n        \"\"\"When one of two workers dies, other still serves requests.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_apps\", [\n            \"tests.support_dirty_app:TestDirtyApp\",\n        ])\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.pid = 12345\n\n        # Set up two workers for the same app\n        arbiter.workers[1001] = \"worker1\"\n        arbiter.worker_sockets[1001] = \"/tmp/fake1.sock\"\n        arbiter.workers[1002] = \"worker2\"\n        arbiter.worker_sockets[1002] = \"/tmp/fake2.sock\"\n\n        app_path = \"tests.support_dirty_app:TestDirtyApp\"\n        arbiter._register_worker_apps(1001, [app_path])\n        arbiter._register_worker_apps(1002, [app_path])\n\n        # Both workers serve the app\n        assert len(arbiter.app_worker_map[app_path]) == 2\n\n        # Worker 1001 crashes\n        arbiter._cleanup_worker(1001)\n\n        # Worker 1002 still serves requests\n        assert len(arbiter.app_worker_map[app_path]) == 1\n        assert 1002 in arbiter.app_worker_map[app_path]\n\n        worker = await arbiter._get_available_worker(app_path)\n        assert worker == 1002\n\n        arbiter._cleanup_sync()\n\n    @pytest.mark.asyncio\n    async def test_worker_crash_sole_worker_app_unavailable_until_respawn(self):\n        \"\"\"When sole worker for app dies, requests fail until respawn.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_apps\", [\n            \"tests.support_dirty_app:SlowDirtyApp:1\",\n        ])\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.pid = 12345\n\n        # Only one worker for this app\n        arbiter.workers[1001] = \"worker1\"\n        arbiter.worker_sockets[1001] = \"/tmp/fake1.sock\"\n\n        app_path = \"tests.support_dirty_app:SlowDirtyApp\"\n        arbiter._register_worker_apps(1001, [app_path])\n\n        # Worker crashes\n        arbiter._cleanup_worker(1001)\n\n        # No workers available for the app\n        worker = await arbiter._get_available_worker(app_path)\n        assert worker is None\n\n        arbiter._cleanup_sync()\n\n    def test_config_format_module_class_n(self):\n        \"\"\"Config 'mod:Class:2' correctly limits to 2 workers.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_apps\", [\n            \"tests.support_dirty_app:TestDirtyApp:2\",\n        ])\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n\n        # Check parsed spec\n        app_path = \"tests.support_dirty_app:TestDirtyApp\"\n        assert arbiter.app_specs[app_path][\"worker_count\"] == 2\n\n        arbiter._cleanup_sync()\n\n    def test_class_attribute_workers_detected(self):\n        \"\"\"App with workers=2 class attribute is detected by arbiter.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_workers\", 4)\n        cfg.set(\"dirty_apps\", [\n            \"tests.support_dirty_app:HeavyModelApp\",  # Has workers=2 class attr\n        ])\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n\n        # Check parsed spec - should read workers=2 from class\n        app_path = \"tests.support_dirty_app:HeavyModelApp\"\n        assert arbiter.app_specs[app_path][\"worker_count\"] == 2\n\n        # Simulate spawning 4 workers\n        for i in range(4):\n            apps = arbiter._get_apps_for_new_worker()\n            arbiter._register_worker_apps(1000 + i, apps)\n\n        # HeavyModelApp should only be on 2 workers\n        assert len(arbiter.app_worker_map[app_path]) == 2\n\n        arbiter._cleanup_sync()\n\n    def test_config_override_takes_precedence_over_class_attribute(self):\n        \"\"\"Config :N takes precedence over class workers attribute.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_workers\", 4)\n        cfg.set(\"dirty_apps\", [\n            # HeavyModelApp has workers=2, but config says 1\n            \"tests.support_dirty_app:HeavyModelApp:1\",\n        ])\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n\n        # Config override (1) should take precedence\n        app_path = \"tests.support_dirty_app:HeavyModelApp\"\n        assert arbiter.app_specs[app_path][\"worker_count\"] == 1\n\n        # Simulate spawning 4 workers\n        for i in range(4):\n            apps = arbiter._get_apps_for_new_worker()\n            arbiter._register_worker_apps(1000 + i, apps)\n\n        # Should only be on 1 worker (config override)\n        assert len(arbiter.app_worker_map[app_path]) == 1\n\n        arbiter._cleanup_sync()\n"
  },
  {
    "path": "tests/dirty/test_streaming_integration.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Integration tests for dirty streaming functionality.\n\nThese tests verify the full streaming pipeline:\nclient -> arbiter -> worker -> generator -> chunks -> client\n\"\"\"\n\nimport asyncio\nimport os\nimport struct\nimport tempfile\nimport pytest\nfrom unittest import mock\n\nfrom gunicorn.config import Config\nfrom gunicorn.dirty.protocol import (\n    DirtyProtocol,\n    BinaryProtocol,\n    make_request,\n    make_chunk_message,\n    make_end_message,\n    make_response,\n    make_error_response,\n    HEADER_SIZE,\n)\nfrom gunicorn.dirty.worker import DirtyWorker\nfrom gunicorn.dirty.arbiter import DirtyArbiter\nfrom gunicorn.dirty.client import DirtyClient\nfrom gunicorn.dirty.errors import DirtyError\n\n\nclass MockLog:\n    \"\"\"Mock logger for testing.\"\"\"\n\n    def __init__(self):\n        self.messages = []\n\n    def debug(self, msg, *args):\n        self.messages.append((\"debug\", msg % args if args else msg))\n\n    def info(self, msg, *args):\n        self.messages.append((\"info\", msg % args if args else msg))\n\n    def warning(self, msg, *args):\n        self.messages.append((\"warning\", msg % args if args else msg))\n\n    def error(self, msg, *args):\n        self.messages.append((\"error\", msg % args if args else msg))\n\n    def close_on_exec(self):\n        pass\n\n    def reopen_files(self):\n        pass\n\n\nclass MockStreamWriter:\n    \"\"\"Mock StreamWriter that captures written messages.\"\"\"\n\n    def __init__(self):\n        self.messages = []\n        self._buffer = b\"\"\n        self.closed = False\n\n    def write(self, data):\n        self._buffer += data\n\n    async def drain(self):\n        # Decode the buffer to extract messages using binary protocol\n        while len(self._buffer) >= HEADER_SIZE:\n            # Decode header to get payload length\n            _, _, length = BinaryProtocol.decode_header(\n                self._buffer[:HEADER_SIZE]\n            )\n            total_size = HEADER_SIZE + length\n            if len(self._buffer) >= total_size:\n                msg_data = self._buffer[:total_size]\n                self._buffer = self._buffer[total_size:]\n                # decode_message returns (msg_type_str, request_id, payload_dict)\n                msg_type_str, request_id, payload_dict = BinaryProtocol.decode_message(msg_data)\n                # Reconstruct the dict format for backwards compatibility\n                result = {\"type\": msg_type_str, \"id\": request_id}\n                result.update(payload_dict)\n                self.messages.append(result)\n            else:\n                break\n\n    def close(self):\n        self.closed = True\n\n    async def wait_closed(self):\n        pass\n\n    def get_extra_info(self, name):\n        return None\n\n\nclass MockStreamReader:\n    \"\"\"Mock StreamReader that yields predefined messages.\"\"\"\n\n    def __init__(self, messages):\n        self._data = b''\n        for msg in messages:\n            self._data += BinaryProtocol._encode_from_dict(msg)\n        self._pos = 0\n\n    async def readexactly(self, n):\n        if self._pos + n > len(self._data):\n            raise asyncio.IncompleteReadError(self._data[self._pos:], n)\n        result = self._data[self._pos:self._pos + n]\n        self._pos += n\n        return result\n\n\nclass TestStreamingEndToEnd:\n    \"\"\"End-to-end streaming tests using mocked components.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_sync_generator_end_to_end(self):\n        \"\"\"Test complete flow: sync generator -> worker -> arbiter -> client.\"\"\"\n        # Simulate what a worker would produce for a sync generator\n        worker_messages = [\n            make_chunk_message(123, \"Hello\"),\n            make_chunk_message(123, \" \"),\n            make_chunk_message(123, \"World\"),\n            make_end_message(123),\n        ]\n\n        # Create an arbiter with mocked worker connection\n        cfg = Config()\n        cfg.set(\"dirty_timeout\", 30)\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.alive = True\n        arbiter.workers = {1234: mock.Mock()}\n        arbiter.worker_sockets = {1234: '/tmp/worker.sock'}\n\n        # Mock worker connection\n        mock_reader = MockStreamReader(worker_messages)\n        async def mock_get_connection(pid):\n            return mock_reader, MockStreamWriter()\n        arbiter._get_worker_connection = mock_get_connection\n\n        # Create client writer to capture messages\n        client_writer = MockStreamWriter()\n\n        # Execute request through arbiter\n        request = make_request(123, \"test:App\", \"generate\")\n        await arbiter._execute_on_worker(1234, request, client_writer)\n\n        # Verify all messages were forwarded\n        assert len(client_writer.messages) == 4\n        assert client_writer.messages[0][\"type\"] == \"chunk\"\n        assert client_writer.messages[0][\"data\"] == \"Hello\"\n        assert client_writer.messages[1][\"data\"] == \" \"\n        assert client_writer.messages[2][\"data\"] == \"World\"\n        assert client_writer.messages[3][\"type\"] == \"end\"\n\n        arbiter._cleanup_sync()\n\n    @pytest.mark.asyncio\n    async def test_async_generator_end_to_end(self):\n        \"\"\"Test complete flow: async generator -> worker -> arbiter -> client.\"\"\"\n        worker_messages = [\n            make_chunk_message(456, \"Async\"),\n            make_chunk_message(456, \" \"),\n            make_chunk_message(456, \"Stream\"),\n            make_end_message(456),\n        ]\n\n        cfg = Config()\n        cfg.set(\"dirty_timeout\", 30)\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.alive = True\n        arbiter.workers = {1234: mock.Mock()}\n        arbiter.worker_sockets = {1234: '/tmp/worker.sock'}\n\n        mock_reader = MockStreamReader(worker_messages)\n        async def mock_get_connection(pid):\n            return mock_reader, MockStreamWriter()\n        arbiter._get_worker_connection = mock_get_connection\n\n        client_writer = MockStreamWriter()\n\n        request = make_request(456, \"test:App\", \"async_generate\")\n        await arbiter._execute_on_worker(1234, request, client_writer)\n\n        assert len(client_writer.messages) == 4\n        assert client_writer.messages[0][\"data\"] == \"Async\"\n        assert client_writer.messages[3][\"type\"] == \"end\"\n\n        arbiter._cleanup_sync()\n\n\nclass TestStreamingErrorHandling:\n    \"\"\"Tests for error handling during streaming.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_error_mid_stream(self):\n        \"\"\"Test that errors during streaming are properly forwarded.\"\"\"\n        worker_messages = [\n            make_chunk_message(789, \"First\"),\n            make_chunk_message(789, \"Second\"),\n            make_error_response(789, DirtyError(\"Stream failed\")),\n        ]\n\n        cfg = Config()\n        cfg.set(\"dirty_timeout\", 30)\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.alive = True\n        arbiter.workers = {1234: mock.Mock()}\n        arbiter.worker_sockets = {1234: '/tmp/worker.sock'}\n\n        mock_reader = MockStreamReader(worker_messages)\n        async def mock_get_connection(pid):\n            return mock_reader, MockStreamWriter()\n        arbiter._get_worker_connection = mock_get_connection\n\n        client_writer = MockStreamWriter()\n\n        request = make_request(789, \"test:App\", \"generate_with_error\")\n        await arbiter._execute_on_worker(1234, request, client_writer)\n\n        # Should have 2 chunks + 1 error\n        assert len(client_writer.messages) == 3\n        assert client_writer.messages[0][\"type\"] == \"chunk\"\n        assert client_writer.messages[1][\"type\"] == \"chunk\"\n        assert client_writer.messages[2][\"type\"] == \"error\"\n        assert \"Stream failed\" in client_writer.messages[2][\"error\"][\"message\"]\n\n        arbiter._cleanup_sync()\n\n\nclass TestStreamingBackwardCompatibility:\n    \"\"\"Tests for backward compatibility with non-streaming responses.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_non_streaming_response_still_works(self):\n        \"\"\"Test that regular (non-streaming) responses still work.\"\"\"\n        worker_messages = [\n            make_response(\"req-abc\", {\"result\": 42, \"data\": [1, 2, 3]}),\n        ]\n\n        cfg = Config()\n        cfg.set(\"dirty_timeout\", 30)\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.alive = True\n        arbiter.workers = {1234: mock.Mock()}\n        arbiter.worker_sockets = {1234: '/tmp/worker.sock'}\n\n        mock_reader = MockStreamReader(worker_messages)\n        async def mock_get_connection(pid):\n            return mock_reader, MockStreamWriter()\n        arbiter._get_worker_connection = mock_get_connection\n\n        client_writer = MockStreamWriter()\n\n        request = make_request(\"req-abc\", \"test:App\", \"compute\")\n        await arbiter._execute_on_worker(1234, request, client_writer)\n\n        # Should have 1 response\n        assert len(client_writer.messages) == 1\n        assert client_writer.messages[0][\"type\"] == \"response\"\n        assert client_writer.messages[0][\"result\"][\"result\"] == 42\n\n        arbiter._cleanup_sync()\n\n    @pytest.mark.asyncio\n    async def test_error_response_still_works(self):\n        \"\"\"Test that error responses still work.\"\"\"\n        worker_messages = [\n            make_error_response(\"req-def\", DirtyError(\"Something failed\")),\n        ]\n\n        cfg = Config()\n        cfg.set(\"dirty_timeout\", 30)\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.alive = True\n        arbiter.workers = {1234: mock.Mock()}\n        arbiter.worker_sockets = {1234: '/tmp/worker.sock'}\n\n        mock_reader = MockStreamReader(worker_messages)\n        async def mock_get_connection(pid):\n            return mock_reader, MockStreamWriter()\n        arbiter._get_worker_connection = mock_get_connection\n\n        client_writer = MockStreamWriter()\n\n        request = make_request(\"req-def\", \"test:App\", \"fail\")\n        await arbiter._execute_on_worker(1234, request, client_writer)\n\n        assert len(client_writer.messages) == 1\n        assert client_writer.messages[0][\"type\"] == \"error\"\n\n        arbiter._cleanup_sync()\n\n\nclass TestStreamingWorkerIntegration:\n    \"\"\"Integration tests for worker streaming with execute.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_worker_handles_sync_generator(self):\n        \"\"\"Test worker properly handles sync generator from execute.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_timeout\", 300)\n        log = MockLog()\n\n        with mock.patch('gunicorn.dirty.worker.WorkerTmp'):\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[\"test:App\"],\n                cfg=cfg,\n                log=log,\n                socket_path=\"/tmp/test.sock\"\n            )\n\n        worker.apps = {}\n        worker._executor = None\n        worker.tmp = mock.Mock()\n\n        writer = MockStreamWriter()\n\n        # Mock execute to return a sync generator\n        def sync_gen():\n            yield \"one\"\n            yield \"two\"\n            yield \"three\"\n\n        async def mock_execute(app_path, action, args, kwargs):\n            return sync_gen()\n\n        with mock.patch.object(worker, 'execute', side_effect=mock_execute):\n            request = make_request(123, \"test:App\", \"generate\")\n            await worker.handle_request(request, writer)\n\n        # Should have 3 chunks + 1 end\n        assert len(writer.messages) == 4\n        assert writer.messages[0][\"data\"] == \"one\"\n        assert writer.messages[1][\"data\"] == \"two\"\n        assert writer.messages[2][\"data\"] == \"three\"\n        assert writer.messages[3][\"type\"] == \"end\"\n\n    @pytest.mark.asyncio\n    async def test_worker_handles_async_generator(self):\n        \"\"\"Test worker properly handles async generator from execute.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_timeout\", 300)\n        log = MockLog()\n\n        with mock.patch('gunicorn.dirty.worker.WorkerTmp'):\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[\"test:App\"],\n                cfg=cfg,\n                log=log,\n                socket_path=\"/tmp/test.sock\"\n            )\n\n        worker.apps = {}\n        worker._executor = None\n        worker.tmp = mock.Mock()\n\n        writer = MockStreamWriter()\n\n        # Mock execute to return an async generator\n        async def async_gen():\n            yield \"async_one\"\n            yield \"async_two\"\n\n        async def mock_execute(app_path, action, args, kwargs):\n            return async_gen()\n\n        with mock.patch.object(worker, 'execute', side_effect=mock_execute):\n            request = make_request(456, \"test:App\", \"async_generate\")\n            await worker.handle_request(request, writer)\n\n        # Should have 2 chunks + 1 end\n        assert len(writer.messages) == 3\n        assert writer.messages[0][\"data\"] == \"async_one\"\n        assert writer.messages[1][\"data\"] == \"async_two\"\n        assert writer.messages[2][\"type\"] == \"end\"\n\n\nclass TestStreamingMixedScenarios:\n    \"\"\"Tests for mixed streaming scenarios.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_large_stream(self):\n        \"\"\"Test streaming with many chunks.\"\"\"\n        worker_messages = []\n        for i in range(500):\n            worker_messages.append(make_chunk_message(\"req-large\", f\"chunk-{i}\"))\n        worker_messages.append(make_end_message(\"req-large\"))\n\n        cfg = Config()\n        cfg.set(\"dirty_timeout\", 30)\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.alive = True\n        arbiter.workers = {1234: mock.Mock()}\n        arbiter.worker_sockets = {1234: '/tmp/worker.sock'}\n\n        mock_reader = MockStreamReader(worker_messages)\n        async def mock_get_connection(pid):\n            return mock_reader, MockStreamWriter()\n        arbiter._get_worker_connection = mock_get_connection\n\n        client_writer = MockStreamWriter()\n\n        request = make_request(\"req-large\", \"test:App\", \"large_stream\")\n        await arbiter._execute_on_worker(1234, request, client_writer)\n\n        # Should have 500 chunks + 1 end\n        assert len(client_writer.messages) == 501\n        assert client_writer.messages[0][\"data\"] == \"chunk-0\"\n        assert client_writer.messages[499][\"data\"] == \"chunk-499\"\n        assert client_writer.messages[500][\"type\"] == \"end\"\n\n        arbiter._cleanup_sync()\n\n    @pytest.mark.asyncio\n    async def test_stream_with_complex_data(self):\n        \"\"\"Test streaming with complex JSON-serializable data.\"\"\"\n        worker_messages = [\n            make_chunk_message(\"req-complex\", {\n                \"token\": \"Hello\",\n                \"scores\": [0.1, 0.2, 0.3],\n                \"metadata\": {\"position\": 0}\n            }),\n            make_chunk_message(\"req-complex\", {\n                \"token\": \"World\",\n                \"scores\": [0.4, 0.5],\n                \"metadata\": {\"position\": 1}\n            }),\n            make_end_message(\"req-complex\"),\n        ]\n\n        cfg = Config()\n        cfg.set(\"dirty_timeout\", 30)\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.alive = True\n        arbiter.workers = {1234: mock.Mock()}\n        arbiter.worker_sockets = {1234: '/tmp/worker.sock'}\n\n        mock_reader = MockStreamReader(worker_messages)\n        async def mock_get_connection(pid):\n            return mock_reader, MockStreamWriter()\n        arbiter._get_worker_connection = mock_get_connection\n\n        client_writer = MockStreamWriter()\n\n        request = make_request(\"req-complex\", \"test:App\", \"complex_stream\")\n        await arbiter._execute_on_worker(1234, request, client_writer)\n\n        assert len(client_writer.messages) == 3\n        assert client_writer.messages[0][\"data\"][\"token\"] == \"Hello\"\n        assert client_writer.messages[0][\"data\"][\"scores\"] == [0.1, 0.2, 0.3]\n        assert client_writer.messages[1][\"data\"][\"metadata\"][\"position\"] == 1\n\n        arbiter._cleanup_sync()\n"
  },
  {
    "path": "tests/dirty/test_worker_streaming.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Tests for dirty worker streaming functionality.\"\"\"\n\nimport asyncio\nimport struct\nfrom unittest import mock\n\nimport pytest\n\nfrom gunicorn.dirty.protocol import (\n    DirtyProtocol,\n    BinaryProtocol,\n    make_request,\n    make_chunk_message,\n    make_end_message,\n    HEADER_SIZE,\n)\nfrom gunicorn.dirty.worker import DirtyWorker\n\n\nclass FakeStreamWriter:\n    \"\"\"Mock StreamWriter that captures written messages.\"\"\"\n\n    def __init__(self):\n        self.messages = []\n        self._buffer = b\"\"\n\n    def write(self, data):\n        self._buffer += data\n\n    async def drain(self):\n        # Decode the buffer to extract messages using binary protocol\n        while len(self._buffer) >= HEADER_SIZE:\n            # Decode header to get payload length\n            _, _, length = BinaryProtocol.decode_header(\n                self._buffer[:HEADER_SIZE]\n            )\n            total_size = HEADER_SIZE + length\n            if len(self._buffer) >= total_size:\n                msg_data = self._buffer[:total_size]\n                self._buffer = self._buffer[total_size:]\n                # decode_message returns (msg_type_str, request_id, payload_dict)\n                msg_type_str, request_id, payload_dict = BinaryProtocol.decode_message(msg_data)\n                # Reconstruct the dict format for backwards compatibility\n                result = {\"type\": msg_type_str, \"id\": request_id}\n                result.update(payload_dict)\n                self.messages.append(result)\n            else:\n                break\n\n    def close(self):\n        pass\n\n    async def wait_closed(self):\n        pass\n\n\ndef create_worker():\n    \"\"\"Create a test worker with mocked components.\"\"\"\n    cfg = mock.Mock()\n    cfg.dirty_timeout = 30\n    cfg.dirty_threads = 1\n    cfg.env = None\n    cfg.uid = None\n    cfg.gid = None\n    cfg.initgroups = False\n    cfg.dirty_worker_init = mock.Mock()\n    cfg.umask = 0o22\n\n    log = mock.Mock()\n\n    with mock.patch('gunicorn.dirty.worker.WorkerTmp'):\n        worker = DirtyWorker(\n            age=1,\n            ppid=1,\n            app_paths=[\"test:App\"],\n            cfg=cfg,\n            log=log,\n            socket_path=\"/tmp/test.sock\"\n        )\n\n    worker.apps = {}\n    worker._executor = None  # Use default executor for sync generator tests\n    worker.tmp = mock.Mock()\n\n    return worker\n\n\nclass TestWorkerSyncGeneratorStreaming:\n    \"\"\"Tests for sync generator streaming.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_sync_generator_sends_chunks_and_end(self):\n        \"\"\"Test that sync generator sends chunk messages then end message.\"\"\"\n        def generate_tokens():\n            yield \"Hello\"\n            yield \" \"\n            yield \"World\"\n\n        worker = create_worker()\n        writer = FakeStreamWriter()\n\n        # Mock execute to return the sync generator directly\n        async def mock_execute(app_path, action, args, kwargs):\n            return generate_tokens()\n\n        with mock.patch.object(worker, 'execute', side_effect=mock_execute):\n            request = make_request(123, \"test:App\", \"generate\")\n            await worker.handle_request(request, writer)\n\n        # Should have 3 chunks + 1 end message\n        assert len(writer.messages) == 4\n\n        # Check chunk messages\n        assert writer.messages[0][\"type\"] == \"chunk\"\n        assert writer.messages[0][\"id\"] == 123\n        assert writer.messages[0][\"data\"] == \"Hello\"\n\n        assert writer.messages[1][\"type\"] == \"chunk\"\n        assert writer.messages[1][\"data\"] == \" \"\n\n        assert writer.messages[2][\"type\"] == \"chunk\"\n        assert writer.messages[2][\"data\"] == \"World\"\n\n        # Check end message\n        assert writer.messages[3][\"type\"] == \"end\"\n        assert writer.messages[3][\"id\"] == 123\n\n    @pytest.mark.asyncio\n    async def test_sync_generator_error_mid_stream(self):\n        \"\"\"Test that error during streaming sends error message.\"\"\"\n        def generate_with_error():\n            yield \"First\"\n            raise ValueError(\"Something went wrong\")\n\n        worker = create_worker()\n        writer = FakeStreamWriter()\n\n        async def mock_execute(app_path, action, args, kwargs):\n            return generate_with_error()\n\n        with mock.patch.object(worker, 'execute', side_effect=mock_execute):\n            request = make_request(123, \"test:App\", \"generate\")\n            await worker.handle_request(request, writer)\n\n        # Should have 1 chunk + 1 error message\n        assert len(writer.messages) == 2\n\n        assert writer.messages[0][\"type\"] == \"chunk\"\n        assert writer.messages[0][\"data\"] == \"First\"\n\n        assert writer.messages[1][\"type\"] == \"error\"\n        assert \"Something went wrong\" in writer.messages[1][\"error\"][\"message\"]\n\n\nclass TestWorkerAsyncGeneratorStreaming:\n    \"\"\"Tests for async generator streaming.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_async_generator_sends_chunks_and_end(self):\n        \"\"\"Test that async generator sends chunk messages then end message.\"\"\"\n        async def async_generate_tokens():\n            yield \"Hello\"\n            yield \" \"\n            yield \"World\"\n\n        worker = create_worker()\n        writer = FakeStreamWriter()\n\n        async def mock_execute(app_path, action, args, kwargs):\n            return async_generate_tokens()\n\n        with mock.patch.object(worker, 'execute', side_effect=mock_execute):\n            request = make_request(123, \"test:App\", \"generate\")\n            await worker.handle_request(request, writer)\n\n        # Should have 3 chunks + 1 end message\n        assert len(writer.messages) == 4\n\n        # Check chunk messages\n        assert writer.messages[0][\"type\"] == \"chunk\"\n        assert writer.messages[0][\"id\"] == 123\n        assert writer.messages[0][\"data\"] == \"Hello\"\n\n        assert writer.messages[1][\"type\"] == \"chunk\"\n        assert writer.messages[1][\"data\"] == \" \"\n\n        assert writer.messages[2][\"type\"] == \"chunk\"\n        assert writer.messages[2][\"data\"] == \"World\"\n\n        # Check end message\n        assert writer.messages[3][\"type\"] == \"end\"\n        assert writer.messages[3][\"id\"] == 123\n\n    @pytest.mark.asyncio\n    async def test_async_generator_error_mid_stream(self):\n        \"\"\"Test that error during async streaming sends error message.\"\"\"\n        async def async_generate_with_error():\n            yield \"First\"\n            raise ValueError(\"Async error\")\n\n        worker = create_worker()\n        writer = FakeStreamWriter()\n\n        async def mock_execute(app_path, action, args, kwargs):\n            return async_generate_with_error()\n\n        with mock.patch.object(worker, 'execute', side_effect=mock_execute):\n            request = make_request(123, \"test:App\", \"generate\")\n            await worker.handle_request(request, writer)\n\n        # Should have 1 chunk + 1 error message\n        assert len(writer.messages) == 2\n\n        assert writer.messages[0][\"type\"] == \"chunk\"\n        assert writer.messages[0][\"data\"] == \"First\"\n\n        assert writer.messages[1][\"type\"] == \"error\"\n        assert \"Async error\" in writer.messages[1][\"error\"][\"message\"]\n\n\nclass TestWorkerNonStreamingBackwardCompat:\n    \"\"\"Tests for backward compatibility with non-streaming responses.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_non_generator_returns_response(self):\n        \"\"\"Test that non-generator method returns regular response.\"\"\"\n        worker = create_worker()\n        writer = FakeStreamWriter()\n\n        async def mock_execute(app_path, action, args, kwargs):\n            return args[0] + args[1]\n\n        with mock.patch.object(worker, 'execute', side_effect=mock_execute):\n            request = make_request(123, \"test:App\", \"compute\", args=(2, 3))\n            await worker.handle_request(request, writer)\n\n        # Should have 1 response message\n        assert len(writer.messages) == 1\n        assert writer.messages[0][\"type\"] == \"response\"\n        assert writer.messages[0][\"id\"] == 123\n        assert writer.messages[0][\"result\"] == 5\n\n    @pytest.mark.asyncio\n    async def test_list_result_not_treated_as_streaming(self):\n        \"\"\"Test that list result is not treated as streaming.\"\"\"\n        worker = create_worker()\n        writer = FakeStreamWriter()\n\n        async def mock_execute(app_path, action, args, kwargs):\n            return [1, 2, 3, 4, 5]\n\n        with mock.patch.object(worker, 'execute', side_effect=mock_execute):\n            request = make_request(123, \"test:App\", \"get_list\")\n            await worker.handle_request(request, writer)\n\n        # Should have 1 response message (not 5 chunks)\n        assert len(writer.messages) == 1\n        assert writer.messages[0][\"type\"] == \"response\"\n        assert writer.messages[0][\"result\"] == [1, 2, 3, 4, 5]\n\n    @pytest.mark.asyncio\n    async def test_error_in_execute_sends_error(self):\n        \"\"\"Test that error in execute sends error response.\"\"\"\n        worker = create_worker()\n        writer = FakeStreamWriter()\n\n        async def mock_execute(app_path, action, args, kwargs):\n            raise RuntimeError(\"Failed!\")\n\n        with mock.patch.object(worker, 'execute', side_effect=mock_execute):\n            request = make_request(123, \"test:App\", \"fail\")\n            await worker.handle_request(request, writer)\n\n        # Should have 1 error message\n        assert len(writer.messages) == 1\n        assert writer.messages[0][\"type\"] == \"error\"\n        assert \"Failed!\" in writer.messages[0][\"error\"][\"message\"]\n\n    @pytest.mark.asyncio\n    async def test_none_result(self):\n        \"\"\"Test that None result works correctly.\"\"\"\n        worker = create_worker()\n        writer = FakeStreamWriter()\n\n        async def mock_execute(app_path, action, args, kwargs):\n            return None\n\n        with mock.patch.object(worker, 'execute', side_effect=mock_execute):\n            request = make_request(123, \"test:App\", \"void\")\n            await worker.handle_request(request, writer)\n\n        # Should have 1 response message\n        assert len(writer.messages) == 1\n        assert writer.messages[0][\"type\"] == \"response\"\n        assert writer.messages[0][\"result\"] is None\n\n\nclass TestWorkerStreamingComplexData:\n    \"\"\"Tests for streaming with complex data types.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_streaming_dict_chunks(self):\n        \"\"\"Test streaming chunks that are dictionaries.\"\"\"\n        async def generate_tokens():\n            yield {\"token\": \"Hello\", \"score\": 0.9}\n            yield {\"token\": \"World\", \"score\": 0.8}\n\n        worker = create_worker()\n        writer = FakeStreamWriter()\n\n        async def mock_execute(app_path, action, args, kwargs):\n            return generate_tokens()\n\n        with mock.patch.object(worker, 'execute', side_effect=mock_execute):\n            request = make_request(123, \"test:App\", \"generate\")\n            await worker.handle_request(request, writer)\n\n        assert len(writer.messages) == 3  # 2 chunks + 1 end\n\n        assert writer.messages[0][\"data\"][\"token\"] == \"Hello\"\n        assert writer.messages[0][\"data\"][\"score\"] == 0.9\n        assert writer.messages[1][\"data\"][\"token\"] == \"World\"\n\n    @pytest.mark.asyncio\n    async def test_streaming_empty_generator(self):\n        \"\"\"Test streaming with empty generator.\"\"\"\n        async def empty_generate():\n            return\n            yield  # Make it a generator\n\n        worker = create_worker()\n        writer = FakeStreamWriter()\n\n        async def mock_execute(app_path, action, args, kwargs):\n            return empty_generate()\n\n        with mock.patch.object(worker, 'execute', side_effect=mock_execute):\n            request = make_request(123, \"test:App\", \"generate\")\n            await worker.handle_request(request, writer)\n\n        # Should have just 1 end message\n        assert len(writer.messages) == 1\n        assert writer.messages[0][\"type\"] == \"end\"\n\n    @pytest.mark.asyncio\n    async def test_streaming_many_chunks(self):\n        \"\"\"Test streaming with many chunks.\"\"\"\n        async def generate_many():\n            for i in range(100):\n                yield f\"chunk-{i}\"\n\n        worker = create_worker()\n        writer = FakeStreamWriter()\n\n        async def mock_execute(app_path, action, args, kwargs):\n            return generate_many()\n\n        with mock.patch.object(worker, 'execute', side_effect=mock_execute):\n            request = make_request(123, \"test:App\", \"generate\")\n            await worker.handle_request(request, writer)\n\n        # Should have 100 chunks + 1 end message\n        assert len(writer.messages) == 101\n        assert writer.messages[0][\"data\"] == \"chunk-0\"\n        assert writer.messages[99][\"data\"] == \"chunk-99\"\n        assert writer.messages[100][\"type\"] == \"end\"\n\n\nclass TestWorkerStreamingHeartbeat:\n    \"\"\"Tests for heartbeat updates during streaming.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_heartbeat_updated_during_streaming(self):\n        \"\"\"Test that heartbeat is updated during streaming.\"\"\"\n        async def generate_tokens():\n            yield \"Hello\"\n            yield \"World\"\n\n        worker = create_worker()\n        writer = FakeStreamWriter()\n\n        # Track notify calls\n        notify_count = [0]\n        original_notify = worker.notify\n\n        def counting_notify():\n            notify_count[0] += 1\n            return original_notify() if callable(original_notify) else None\n\n        worker.notify = counting_notify\n\n        async def mock_execute(app_path, action, args, kwargs):\n            return generate_tokens()\n\n        with mock.patch.object(worker, 'execute', side_effect=mock_execute):\n            request = make_request(123, \"test:App\", \"generate\")\n            await worker.handle_request(request, writer)\n\n        # Should have been notified at least once per chunk + initial\n        assert notify_count[0] >= 2  # At least one per chunk\n\n\nclass TestWorkerMessageTypeValidation:\n    \"\"\"Tests for message type validation.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_unknown_message_type_sends_error(self):\n        \"\"\"Test that unknown message type sends error response.\"\"\"\n        worker = create_worker()\n        writer = FakeStreamWriter()\n\n        # Send a message with unknown type\n        message = {\"type\": \"unknown\", \"id\": 123}\n        await worker.handle_request(message, writer)\n\n        assert len(writer.messages) == 1\n        assert writer.messages[0][\"type\"] == \"error\"\n        assert \"Unknown message type\" in writer.messages[0][\"error\"][\"message\"]\n"
  },
  {
    "path": "tests/docker/__init__.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Docker-based integration tests package.\"\"\"\n"
  },
  {
    "path": "tests/docker/asgi/Dockerfile",
    "content": "FROM python:3.11-slim\n\nWORKDIR /build\n\n# Copy gunicorn source\nCOPY . /build/\n\n# Install gunicorn from source\nRUN pip install --no-cache-dir -e .\n\n# Copy test app\nWORKDIR /app\nCOPY tests/docker/asgi/app.py /app/\n\n# Expose HTTP port\nEXPOSE 8000\n\nCMD [\"gunicorn\", \"--worker-class\", \"asgi\", \"--bind\", \"0.0.0.0:8000\", \"app:app\"]\n"
  },
  {
    "path": "tests/docker/asgi/app.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Simple ASGI test application for HTTP protocol testing.\"\"\"\n\n\nasync def app(scope, receive, send):\n    \"\"\"Simple ASGI application that echoes request info.\"\"\"\n    if scope[\"type\"] == \"lifespan\":\n        while True:\n            message = await receive()\n            if message[\"type\"] == \"lifespan.startup\":\n                await send({\"type\": \"lifespan.startup.complete\"})\n            elif message[\"type\"] == \"lifespan.shutdown\":\n                await send({\"type\": \"lifespan.shutdown.complete\"})\n                return\n\n    if scope[\"type\"] != \"http\":\n        return\n\n    # Read body\n    body = b\"\"\n    while True:\n        message = await receive()\n        body += message.get(\"body\", b\"\")\n        if not message.get(\"more_body\", False):\n            break\n\n    # Build response\n    method = scope[\"method\"]\n    path = scope[\"path\"]\n    query = scope.get(\"query_string\", b\"\").decode(\"utf-8\")\n\n    response_body = f\"Method: {method}\\nPath: {path}\\nQuery: {query}\\nBody: {body.decode('utf-8')}\\n\"\n    response_bytes = response_body.encode(\"utf-8\")\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 200,\n        \"headers\": [\n            [b\"content-type\", b\"text/plain\"],\n            [b\"content-length\", str(len(response_bytes)).encode()],\n        ],\n    })\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": response_bytes,\n    })\n"
  },
  {
    "path": "tests/docker/asgi/docker-compose.yml",
    "content": "services:\n  gunicorn:\n    build:\n      context: ../../..\n      dockerfile: tests/docker/asgi/Dockerfile\n    command: >\n      gunicorn\n      --worker-class asgi\n      --bind 0.0.0.0:8000\n      --workers 1\n      --log-level debug\n      app:app\n    ports:\n      - \"8080:8000\"\n"
  },
  {
    "path": "tests/docker/asgi/test_asgi.sh",
    "content": "#!/bin/bash\n# Integration test for ASGI HTTP protocol support\n#\n# This script tests that gunicorn's ASGI worker correctly handles\n# HTTP requests directly (without uWSGI protocol).\n\nset -e\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\ncd \"$SCRIPT_DIR\"\n\n# Use IPv4 explicitly to avoid Docker IPv6 issues\nBASE_URL=\"http://127.0.0.1:8080\"\n\ncleanup() {\n    echo \"Cleaning up...\"\n    docker compose down -v 2>/dev/null || true\n}\n\ntrap cleanup EXIT\n\necho \"=== Building and starting containers ===\"\ndocker compose up -d --build\n\necho \"=== Waiting for services to be ready ===\"\nsleep 5\n\necho \"=== Running tests ===\"\n\n# Test 1: Simple GET request\necho \"Test 1: Simple GET request\"\nRESPONSE=$(curl -s \"$BASE_URL/\")\nif echo \"$RESPONSE\" | grep -q \"Method: GET\"; then\n    echo \"  PASS: GET request works\"\nelse\n    echo \"  FAIL: GET request failed\"\n    echo \"  Response: $RESPONSE\"\n    exit 1\nfi\n\n# Test 2: GET with query string\necho \"Test 2: GET with query string\"\nRESPONSE=$(curl -s \"$BASE_URL/search?q=test&page=1\")\nif echo \"$RESPONSE\" | grep -q \"Query: q=test&page=1\"; then\n    echo \"  PASS: Query string works\"\nelse\n    echo \"  FAIL: Query string failed\"\n    echo \"  Response: $RESPONSE\"\n    exit 1\nfi\n\n# Test 3: POST with body\necho \"Test 3: POST with body\"\nRESPONSE=$(curl -s -X POST -d \"hello=world\" \"$BASE_URL/submit\")\nif echo \"$RESPONSE\" | grep -q \"Method: POST\" && echo \"$RESPONSE\" | grep -q \"Body: hello=world\"; then\n    echo \"  PASS: POST with body works\"\nelse\n    echo \"  FAIL: POST with body failed\"\n    echo \"  Response: $RESPONSE\"\n    exit 1\nfi\n\n# Test 4: Path handling\necho \"Test 4: Path handling\"\nRESPONSE=$(curl -s \"$BASE_URL/api/v1/users\")\nif echo \"$RESPONSE\" | grep -q \"Path: /api/v1/users\"; then\n    echo \"  PASS: Path handling works\"\nelse\n    echo \"  FAIL: Path handling failed\"\n    echo \"  Response: $RESPONSE\"\n    exit 1\nfi\n\n# Test 5: Multiple requests (keepalive)\necho \"Test 5: Multiple requests (keepalive)\"\nfor i in 1 2 3; do\n    RESPONSE=$(curl -s \"$BASE_URL/request/$i\")\n    if ! echo \"$RESPONSE\" | grep -q \"Path: /request/$i\"; then\n        echo \"  FAIL: Request $i failed\"\n        exit 1\n    fi\ndone\necho \"  PASS: Multiple requests work\"\n\n# Test 6: Large POST body\necho \"Test 6: Large POST body\"\nLARGE_BODY=$(python3 -c \"print('x' * 10000)\")\nRESPONSE=$(curl -s -X POST -d \"$LARGE_BODY\" \"$BASE_URL/large\")\nif echo \"$RESPONSE\" | grep -q \"Method: POST\" && echo \"$RESPONSE\" | grep -c \"x\" | grep -q \"10000\"; then\n    echo \"  PASS: Large POST body works\"\nelse\n    # Verify body length in response\n    BODY_LINE=$(echo \"$RESPONSE\" | grep \"Body:\")\n    BODY_LEN=${#BODY_LINE}\n    if [ \"$BODY_LEN\" -gt 10000 ]; then\n        echo \"  PASS: Large POST body works\"\n    else\n        echo \"  FAIL: Large POST body failed\"\n        echo \"  Response length: $BODY_LEN\"\n        exit 1\n    fi\nfi\n\n# Test 7: HTTP headers\necho \"Test 7: Custom headers\"\nRESPONSE=$(curl -s -H \"X-Custom-Header: test-value\" \"$BASE_URL/headers\")\nif echo \"$RESPONSE\" | grep -q \"Method: GET\"; then\n    echo \"  PASS: Custom headers work\"\nelse\n    echo \"  FAIL: Custom headers failed\"\n    echo \"  Response: $RESPONSE\"\n    exit 1\nfi\n\necho \"\"\necho \"=== All tests passed! ===\"\n"
  },
  {
    "path": "tests/docker/asgi_compliance/Dockerfile.gunicorn",
    "content": "FROM python:3.12-slim\n\n# Install build dependencies\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n    gcc \\\n    curl \\\n    && rm -rf /var/lib/apt/lists/*\n\nWORKDIR /app\n\n# Copy the gunicorn source code and install it with all extras\nCOPY . /gunicorn-src/\nRUN pip install --no-cache-dir /gunicorn-src/[http2,testing]\n\n# Install additional ASGI frameworks for testing\nRUN pip install --no-cache-dir \\\n    starlette>=0.35.0 \\\n    fastapi>=0.109.0 \\\n    websockets>=12.0\n\n# Copy the test applications\nCOPY tests/docker/asgi_compliance/apps /app/apps\n\n# Create entrypoint script\nRUN echo '#!/bin/bash\\n\\\nset -e\\n\\\n\\n\\\nif [ \"$USE_SSL\" = \"1\" ]; then\\n\\\n    exec gunicorn \"apps.main_app:app\" \\\\\\n\\\n        --bind \"[::]:8443\" \\\\\\n\\\n        --worker-class \"asgi\" \\\\\\n\\\n        --workers 2 \\\\\\n\\\n        --worker-connections 1000 \\\\\\n\\\n        --certfile \"/certs/server.crt\" \\\\\\n\\\n        --keyfile \"/certs/server.key\" \\\\\\n\\\n        --asgi-disconnect-grace-period 0 \\\\\\n\\\n        --log-level \"debug\" \\\\\\n\\\n        --access-logfile \"-\" \\\\\\n\\\n        --error-logfile \"-\"\\n\\\nelse\\n\\\n    exec gunicorn \"apps.main_app:app\" \\\\\\n\\\n        --bind \"[::]:8000\" \\\\\\n\\\n        --worker-class \"asgi\" \\\\\\n\\\n        --workers 2 \\\\\\n\\\n        --worker-connections 1000 \\\\\\n\\\n        --asgi-disconnect-grace-period 0 \\\\\\n\\\n        --log-level \"debug\" \\\\\\n\\\n        --access-logfile \"-\" \\\\\\n\\\n        --error-logfile \"-\"\\n\\\nfi\\n\\\n' > /app/entrypoint.sh && chmod +x /app/entrypoint.sh\n\nEXPOSE 8000 8443\n\nCMD [\"/app/entrypoint.sh\"]\n"
  },
  {
    "path": "tests/docker/asgi_compliance/Dockerfile.nginx",
    "content": "FROM nginx:1.25-alpine\n\n# Install curl for health checks\nRUN apk add --no-cache curl\n\n# Remove default config\nRUN rm /etc/nginx/conf.d/default.conf\n\n# Copy custom nginx config\nCOPY nginx.conf /etc/nginx/nginx.conf\n\nEXPOSE 8080 8444\n\nCMD [\"nginx\", \"-g\", \"daemon off;\"]\n"
  },
  {
    "path": "tests/docker/asgi_compliance/__init__.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nASGI Compliance Docker Integration Tests.\n\nThis package contains Docker-based integration tests for ASGI compliance.\n\"\"\"\n"
  },
  {
    "path": "tests/docker/asgi_compliance/apps/__init__.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nASGI test applications for compliance testing.\n\"\"\"\n"
  },
  {
    "path": "tests/docker/asgi_compliance/apps/framework_apps.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nFramework integration test applications.\n\nTests integration with popular ASGI frameworks like Starlette and FastAPI.\nThese apps require the frameworks to be installed.\n\"\"\"\n\nimport json\nimport os\n\n# Framework availability flags\nSTARLETTE_AVAILABLE = False\nFASTAPI_AVAILABLE = False\n\ntry:\n    from starlette.applications import Starlette\n    from starlette.responses import (\n        JSONResponse,\n        PlainTextResponse,\n        StreamingResponse,\n    )\n    from starlette.routing import Route, WebSocketRoute\n    from starlette.websockets import WebSocket\n    STARLETTE_AVAILABLE = True\nexcept ImportError:\n    pass\n\ntry:\n    from fastapi import FastAPI, Request, WebSocket as FastAPIWebSocket\n    from fastapi.responses import (\n        JSONResponse as FastAPIJSONResponse,\n        StreamingResponse as FastAPIStreamingResponse,\n    )\n    FASTAPI_AVAILABLE = True\nexcept ImportError:\n    pass\n\n\n# ============================================================================\n# Pure ASGI Fallback App (when frameworks not available)\n# ============================================================================\n\nasync def fallback_app(scope, receive, send):\n    \"\"\"Fallback ASGI app when frameworks are not installed.\"\"\"\n    if scope[\"type\"] == \"lifespan\":\n        while True:\n            message = await receive()\n            if message[\"type\"] == \"lifespan.startup\":\n                await send({\"type\": \"lifespan.startup.complete\"})\n            elif message[\"type\"] == \"lifespan.shutdown\":\n                await send({\"type\": \"lifespan.shutdown.complete\"})\n                return\n        return\n\n    if scope[\"type\"] != \"http\":\n        return\n\n    body = json.dumps({\n        \"error\": \"Framework not available\",\n        \"starlette_available\": STARLETTE_AVAILABLE,\n        \"fastapi_available\": FASTAPI_AVAILABLE,\n        \"message\": \"Install starlette and/or fastapi to use this app\",\n    }).encode(\"utf-8\")\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 503,\n        \"headers\": [\n            (b\"content-type\", b\"application/json\"),\n            (b\"content-length\", str(len(body)).encode()),\n        ],\n    })\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": body,\n        \"more_body\": False,\n    })\n\n\n# ============================================================================\n# Starlette Application\n# ============================================================================\n\nif STARLETTE_AVAILABLE:\n    import asyncio\n\n    async def starlette_homepage(request):\n        \"\"\"Starlette homepage.\"\"\"\n        return PlainTextResponse(\"Hello from Starlette!\")\n\n    async def starlette_json(request):\n        \"\"\"Return JSON response.\"\"\"\n        return JSONResponse({\n            \"framework\": \"starlette\",\n            \"method\": request.method,\n            \"path\": request.url.path,\n            \"query_params\": dict(request.query_params),\n        })\n\n    async def starlette_echo(request):\n        \"\"\"Echo request body.\"\"\"\n        body = await request.body()\n        return PlainTextResponse(body.decode(\"utf-8\", errors=\"replace\"))\n\n    async def starlette_headers(request):\n        \"\"\"Return request headers.\"\"\"\n        return JSONResponse(dict(request.headers))\n\n    async def starlette_scope(request):\n        \"\"\"Return ASGI scope.\"\"\"\n        scope = request.scope\n        scope_json = {\n            \"type\": scope[\"type\"],\n            \"asgi\": scope[\"asgi\"],\n            \"http_version\": scope[\"http_version\"],\n            \"method\": scope[\"method\"],\n            \"scheme\": scope[\"scheme\"],\n            \"path\": scope[\"path\"],\n            \"query_string\": scope[\"query_string\"].decode(\"latin-1\"),\n            \"root_path\": scope.get(\"root_path\", \"\"),\n            \"headers\": [\n                [n.decode(\"latin-1\"), v.decode(\"latin-1\")]\n                for n, v in scope[\"headers\"]\n            ],\n            \"server\": list(scope[\"server\"]) if scope.get(\"server\") else None,\n            \"client\": list(scope[\"client\"]) if scope.get(\"client\") else None,\n        }\n        return JSONResponse(scope_json)\n\n    async def starlette_streaming(request):\n        \"\"\"Streaming response.\"\"\"\n        async def generate():\n            for i in range(10):\n                yield f\"Chunk {i + 1}\\n\".encode(\"utf-8\")\n                await asyncio.sleep(0.1)\n\n        return StreamingResponse(generate(), media_type=\"text/plain\")\n\n    async def starlette_websocket_endpoint(websocket: WebSocket):\n        \"\"\"WebSocket echo endpoint.\"\"\"\n        await websocket.accept()\n        try:\n            while True:\n                data = await websocket.receive_text()\n                await websocket.send_text(f\"Starlette echo: {data}\")\n        except Exception:\n            pass\n\n    async def starlette_health(request):\n        \"\"\"Health check.\"\"\"\n        return PlainTextResponse(\"OK\")\n\n    # Lifespan context manager\n    from contextlib import asynccontextmanager\n\n    @asynccontextmanager\n    async def starlette_lifespan(app):\n        \"\"\"Starlette lifespan context manager.\"\"\"\n        # Startup\n        app.state.startup_time = asyncio.get_event_loop().time()\n        app.state.started = True\n        yield\n        # Shutdown\n        app.state.started = False\n\n    starlette_routes = [\n        Route(\"/\", starlette_homepage),\n        Route(\"/json\", starlette_json),\n        Route(\"/echo\", starlette_echo, methods=[\"POST\"]),\n        Route(\"/headers\", starlette_headers),\n        Route(\"/scope\", starlette_scope),\n        Route(\"/streaming\", starlette_streaming),\n        Route(\"/health\", starlette_health),\n        WebSocketRoute(\"/ws/echo\", starlette_websocket_endpoint),\n    ]\n\n    starlette_app = Starlette(\n        routes=starlette_routes,\n        lifespan=starlette_lifespan,\n    )\nelse:\n    starlette_app = fallback_app\n\n\n# ============================================================================\n# FastAPI Application\n# ============================================================================\n\nif FASTAPI_AVAILABLE:\n    import asyncio\n    from contextlib import asynccontextmanager\n    from typing import Any, Dict\n\n    @asynccontextmanager\n    async def fastapi_lifespan(app: FastAPI):\n        \"\"\"FastAPI lifespan context manager.\"\"\"\n        # Startup\n        app.state.startup_time = asyncio.get_event_loop().time()\n        app.state.started = True\n        yield\n        # Shutdown\n        app.state.started = False\n\n    fastapi_app = FastAPI(\n        title=\"ASGI Compliance Test - FastAPI\",\n        lifespan=fastapi_lifespan,\n    )\n\n    @fastapi_app.get(\"/\")\n    async def fastapi_root():\n        \"\"\"FastAPI homepage.\"\"\"\n        return {\"message\": \"Hello from FastAPI!\"}\n\n    @fastapi_app.get(\"/json\")\n    async def fastapi_json(request: Request) -> Dict[str, Any]:\n        \"\"\"Return JSON response with request info.\"\"\"\n        return {\n            \"framework\": \"fastapi\",\n            \"method\": request.method,\n            \"path\": str(request.url.path),\n            \"query_params\": dict(request.query_params),\n        }\n\n    @fastapi_app.post(\"/echo\")\n    async def fastapi_echo(request: Request):\n        \"\"\"Echo request body.\"\"\"\n        body = await request.body()\n        return FastAPIJSONResponse(content={\n            \"echo\": body.decode(\"utf-8\", errors=\"replace\"),\n            \"length\": len(body),\n        })\n\n    @fastapi_app.get(\"/headers\")\n    async def fastapi_headers(request: Request):\n        \"\"\"Return request headers.\"\"\"\n        return dict(request.headers)\n\n    @fastapi_app.get(\"/scope\")\n    async def fastapi_scope(request: Request):\n        \"\"\"Return ASGI scope.\"\"\"\n        scope = request.scope\n        return {\n            \"type\": scope[\"type\"],\n            \"asgi\": scope[\"asgi\"],\n            \"http_version\": scope[\"http_version\"],\n            \"method\": scope[\"method\"],\n            \"scheme\": scope[\"scheme\"],\n            \"path\": scope[\"path\"],\n            \"query_string\": scope[\"query_string\"].decode(\"latin-1\"),\n            \"root_path\": scope.get(\"root_path\", \"\"),\n            \"server\": list(scope[\"server\"]) if scope.get(\"server\") else None,\n            \"client\": list(scope[\"client\"]) if scope.get(\"client\") else None,\n        }\n\n    @fastapi_app.get(\"/streaming\")\n    async def fastapi_streaming():\n        \"\"\"Streaming response.\"\"\"\n        async def generate():\n            for i in range(10):\n                yield f\"Chunk {i + 1}\\n\"\n                await asyncio.sleep(0.1)\n\n        return FastAPIStreamingResponse(generate(), media_type=\"text/plain\")\n\n    @fastapi_app.get(\"/health\")\n    async def fastapi_health():\n        \"\"\"Health check.\"\"\"\n        return {\"status\": \"ok\"}\n\n    @fastapi_app.get(\"/items/{item_id}\")\n    async def fastapi_get_item(item_id: int, q: str = None):\n        \"\"\"Path parameter example.\"\"\"\n        return {\"item_id\": item_id, \"query\": q}\n\n    @fastapi_app.post(\"/items/\")\n    async def fastapi_create_item(request: Request):\n        \"\"\"Create item example.\"\"\"\n        body = await request.json()\n        return {\"created\": body}\n\n    @fastapi_app.websocket(\"/ws/echo\")\n    async def fastapi_websocket_echo(websocket: FastAPIWebSocket):\n        \"\"\"WebSocket echo endpoint.\"\"\"\n        await websocket.accept()\n        try:\n            while True:\n                data = await websocket.receive_text()\n                await websocket.send_text(f\"FastAPI echo: {data}\")\n        except Exception:\n            pass\n\nelse:\n    fastapi_app = fallback_app\n\n\n# ============================================================================\n# Combined Application Router\n# ============================================================================\n\nasync def combined_app(scope, receive, send):\n    \"\"\"Combined app that routes based on path prefix.\"\"\"\n    if scope[\"type\"] == \"lifespan\":\n        # Handle lifespan for both apps\n        while True:\n            message = await receive()\n            if message[\"type\"] == \"lifespan.startup\":\n                await send({\"type\": \"lifespan.startup.complete\"})\n            elif message[\"type\"] == \"lifespan.shutdown\":\n                await send({\"type\": \"lifespan.shutdown.complete\"})\n                return\n        return\n\n    path = scope.get(\"path\", \"\")\n\n    if path.startswith(\"/starlette\"):\n        # Strip prefix for Starlette\n        scope = dict(scope)\n        scope[\"path\"] = path[10:] or \"/\"\n        scope[\"raw_path\"] = scope[\"path\"].encode(\"latin-1\")\n        await starlette_app(scope, receive, send)\n    elif path.startswith(\"/fastapi\"):\n        # Strip prefix for FastAPI\n        scope = dict(scope)\n        scope[\"path\"] = path[8:] or \"/\"\n        scope[\"raw_path\"] = scope[\"path\"].encode(\"latin-1\")\n        await fastapi_app(scope, receive, send)\n    elif path == \"/\":\n        # Root - show available apps\n        body = json.dumps({\n            \"apps\": {\n                \"starlette\": {\n                    \"available\": STARLETTE_AVAILABLE,\n                    \"prefix\": \"/starlette\",\n                },\n                \"fastapi\": {\n                    \"available\": FASTAPI_AVAILABLE,\n                    \"prefix\": \"/fastapi\",\n                },\n            },\n            \"endpoints\": {\n                \"starlette\": [\"/\", \"/json\", \"/echo\", \"/headers\", \"/scope\", \"/streaming\", \"/ws/echo\"],\n                \"fastapi\": [\"/\", \"/json\", \"/echo\", \"/headers\", \"/scope\", \"/streaming\", \"/items/{id}\", \"/ws/echo\"],\n            },\n        }).encode(\"utf-8\")\n\n        await send({\n            \"type\": \"http.response.start\",\n            \"status\": 200,\n            \"headers\": [\n                (b\"content-type\", b\"application/json\"),\n                (b\"content-length\", str(len(body)).encode()),\n            ],\n        })\n        await send({\n            \"type\": \"http.response.body\",\n            \"body\": body,\n            \"more_body\": False,\n        })\n    elif path == \"/health\":\n        body = b\"OK\"\n        await send({\n            \"type\": \"http.response.start\",\n            \"status\": 200,\n            \"headers\": [\n                (b\"content-type\", b\"text/plain\"),\n                (b\"content-length\", str(len(body)).encode()),\n            ],\n        })\n        await send({\n            \"type\": \"http.response.body\",\n            \"body\": body,\n            \"more_body\": False,\n        })\n    else:\n        body = b\"Not Found - use /starlette/* or /fastapi/* prefixes\"\n        await send({\n            \"type\": \"http.response.start\",\n            \"status\": 404,\n            \"headers\": [\n                (b\"content-type\", b\"text/plain\"),\n                (b\"content-length\", str(len(body)).encode()),\n            ],\n        })\n        await send({\n            \"type\": \"http.response.body\",\n            \"body\": body,\n            \"more_body\": False,\n        })\n\n\n# Export the apps\napp = combined_app\n"
  },
  {
    "path": "tests/docker/asgi_compliance/apps/http_app.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nHTTP test application for ASGI compliance testing.\n\nProvides various endpoints to test HTTP request/response handling,\nheaders, body processing, and ASGI scope inspection.\n\"\"\"\n\nimport json\nimport time\n\n\nasync def app(scope, receive, send):\n    \"\"\"Main ASGI HTTP application with multiple test endpoints.\"\"\"\n    if scope[\"type\"] == \"lifespan\":\n        await handle_lifespan(scope, receive, send)\n        return\n\n    if scope[\"type\"] != \"http\":\n        return\n\n    path = scope[\"path\"]\n    method = scope[\"method\"]\n\n    # Route to appropriate handler\n    if path == \"/\":\n        await handle_root(scope, receive, send)\n    elif path == \"/echo\":\n        await handle_echo(scope, receive, send)\n    elif path == \"/headers\":\n        await handle_headers(scope, receive, send)\n    elif path == \"/scope\":\n        await handle_scope(scope, receive, send)\n    elif path.startswith(\"/status\"):\n        await handle_status(scope, receive, send)\n    elif path == \"/large\":\n        await handle_large(scope, receive, send)\n    elif path == \"/method\":\n        await handle_method(scope, receive, send)\n    elif path == \"/query\":\n        await handle_query(scope, receive, send)\n    elif path == \"/post-json\":\n        await handle_post_json(scope, receive, send)\n    elif path == \"/delay\":\n        await handle_delay(scope, receive, send)\n    elif path == \"/health\":\n        await handle_health(scope, receive, send)\n    elif path == \"/early-hints\":\n        await handle_early_hints(scope, receive, send)\n    elif path == \"/cookies\":\n        await handle_cookies(scope, receive, send)\n    elif path == \"/redirect\":\n        await handle_redirect(scope, receive, send)\n    else:\n        await handle_not_found(scope, receive, send)\n\n\nasync def handle_lifespan(scope, receive, send):\n    \"\"\"Handle ASGI lifespan events.\"\"\"\n    while True:\n        message = await receive()\n        if message[\"type\"] == \"lifespan.startup\":\n            # Store startup time in state if available\n            if \"state\" in scope:\n                scope[\"state\"][\"started_at\"] = time.time()\n            await send({\"type\": \"lifespan.startup.complete\"})\n        elif message[\"type\"] == \"lifespan.shutdown\":\n            await send({\"type\": \"lifespan.shutdown.complete\"})\n            return\n\n\nasync def handle_root(scope, receive, send):\n    \"\"\"Handle root path - basic response.\"\"\"\n    body = b\"Hello, ASGI!\"\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 200,\n        \"headers\": [\n            (b\"content-type\", b\"text/plain; charset=utf-8\"),\n            (b\"content-length\", str(len(body)).encode()),\n        ],\n    })\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": body,\n        \"more_body\": False,\n    })\n\n\nasync def handle_echo(scope, receive, send):\n    \"\"\"Echo back the request body.\"\"\"\n    # Read the full request body\n    body_parts = []\n    while True:\n        message = await receive()\n        body = message.get(\"body\", b\"\")\n        if body:\n            body_parts.append(body)\n        if not message.get(\"more_body\", False):\n            break\n\n    response_body = b\"\".join(body_parts)\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 200,\n        \"headers\": [\n            (b\"content-type\", b\"application/octet-stream\"),\n            (b\"content-length\", str(len(response_body)).encode()),\n        ],\n    })\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": response_body,\n        \"more_body\": False,\n    })\n\n\nasync def handle_headers(scope, receive, send):\n    \"\"\"Return request headers as JSON.\"\"\"\n    # Drain request body\n    await drain_body(receive)\n\n    # Convert headers to JSON-serializable format\n    headers_dict = {}\n    for name, value in scope[\"headers\"]:\n        name_str = name.decode(\"latin-1\")\n        value_str = value.decode(\"latin-1\")\n        if name_str in headers_dict:\n            # Handle multiple headers with same name\n            if isinstance(headers_dict[name_str], list):\n                headers_dict[name_str].append(value_str)\n            else:\n                headers_dict[name_str] = [headers_dict[name_str], value_str]\n        else:\n            headers_dict[name_str] = value_str\n\n    response_body = json.dumps(headers_dict, indent=2).encode(\"utf-8\")\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 200,\n        \"headers\": [\n            (b\"content-type\", b\"application/json\"),\n            (b\"content-length\", str(len(response_body)).encode()),\n        ],\n    })\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": response_body,\n        \"more_body\": False,\n    })\n\n\nasync def handle_scope(scope, receive, send):\n    \"\"\"Return ASGI scope as JSON for inspection.\"\"\"\n    # Drain request body\n    await drain_body(receive)\n\n    # Create a JSON-serializable version of the scope\n    scope_json = {\n        \"type\": scope[\"type\"],\n        \"asgi\": scope[\"asgi\"],\n        \"http_version\": scope[\"http_version\"],\n        \"method\": scope[\"method\"],\n        \"scheme\": scope[\"scheme\"],\n        \"path\": scope[\"path\"],\n        \"raw_path\": scope[\"raw_path\"].decode(\"latin-1\") if scope.get(\"raw_path\") else None,\n        \"query_string\": scope[\"query_string\"].decode(\"latin-1\") if scope.get(\"query_string\") else \"\",\n        \"root_path\": scope.get(\"root_path\", \"\"),\n        \"headers\": [\n            [name.decode(\"latin-1\"), value.decode(\"latin-1\")]\n            for name, value in scope[\"headers\"]\n        ],\n        \"server\": list(scope[\"server\"]) if scope.get(\"server\") else None,\n        \"client\": list(scope[\"client\"]) if scope.get(\"client\") else None,\n    }\n\n    # Include extensions if present\n    if \"extensions\" in scope:\n        scope_json[\"extensions\"] = {}\n        for ext_name, ext_value in scope[\"extensions\"].items():\n            if isinstance(ext_value, dict):\n                scope_json[\"extensions\"][ext_name] = ext_value\n            else:\n                scope_json[\"extensions\"][ext_name] = str(ext_value)\n\n    # Include state keys if present\n    if \"state\" in scope:\n        scope_json[\"state_keys\"] = list(scope[\"state\"].keys())\n\n    response_body = json.dumps(scope_json, indent=2).encode(\"utf-8\")\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 200,\n        \"headers\": [\n            (b\"content-type\", b\"application/json\"),\n            (b\"content-length\", str(len(response_body)).encode()),\n        ],\n    })\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": response_body,\n        \"more_body\": False,\n    })\n\n\nasync def handle_status(scope, receive, send):\n    \"\"\"Return specific HTTP status code from query parameter.\"\"\"\n    # Drain request body\n    await drain_body(receive)\n\n    # Parse query string for status code\n    query = scope[\"query_string\"].decode(\"latin-1\")\n    status_code = 200\n\n    for param in query.split(\"&\"):\n        if param.startswith(\"code=\"):\n            try:\n                status_code = int(param[5:])\n            except ValueError:\n                status_code = 400\n\n    # Status code validation\n    if status_code < 100 or status_code >= 600:\n        status_code = 400\n\n    body = f\"Status: {status_code}\".encode(\"utf-8\")\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": status_code,\n        \"headers\": [\n            (b\"content-type\", b\"text/plain\"),\n            (b\"content-length\", str(len(body)).encode()),\n        ],\n    })\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": body,\n        \"more_body\": False,\n    })\n\n\nasync def handle_large(scope, receive, send):\n    \"\"\"Return a large response (1MB by default).\"\"\"\n    # Drain request body\n    await drain_body(receive)\n\n    # Parse query string for size\n    query = scope[\"query_string\"].decode(\"latin-1\")\n    size = 1024 * 1024  # 1MB default\n\n    for param in query.split(\"&\"):\n        if param.startswith(\"size=\"):\n            try:\n                size = int(param[5:])\n                # Limit to 10MB\n                size = min(size, 10 * 1024 * 1024)\n            except ValueError:\n                pass\n\n    # Generate response body\n    body = b\"x\" * size\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 200,\n        \"headers\": [\n            (b\"content-type\", b\"application/octet-stream\"),\n            (b\"content-length\", str(len(body)).encode()),\n        ],\n    })\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": body,\n        \"more_body\": False,\n    })\n\n\nasync def handle_method(scope, receive, send):\n    \"\"\"Return the HTTP method used.\"\"\"\n    # Drain request body\n    await drain_body(receive)\n\n    method = scope[\"method\"]\n    body = json.dumps({\"method\": method}).encode(\"utf-8\")\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 200,\n        \"headers\": [\n            (b\"content-type\", b\"application/json\"),\n            (b\"content-length\", str(len(body)).encode()),\n        ],\n    })\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": body,\n        \"more_body\": False,\n    })\n\n\nasync def handle_query(scope, receive, send):\n    \"\"\"Return parsed query parameters.\"\"\"\n    # Drain request body\n    await drain_body(receive)\n\n    query = scope[\"query_string\"].decode(\"latin-1\")\n    params = {}\n\n    if query:\n        for param in query.split(\"&\"):\n            if \"=\" in param:\n                key, value = param.split(\"=\", 1)\n                # Handle multiple values for same key\n                if key in params:\n                    if isinstance(params[key], list):\n                        params[key].append(value)\n                    else:\n                        params[key] = [params[key], value]\n                else:\n                    params[key] = value\n            else:\n                params[param] = \"\"\n\n    body = json.dumps({\n        \"raw\": query,\n        \"params\": params,\n    }).encode(\"utf-8\")\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 200,\n        \"headers\": [\n            (b\"content-type\", b\"application/json\"),\n            (b\"content-length\", str(len(body)).encode()),\n        ],\n    })\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": body,\n        \"more_body\": False,\n    })\n\n\nasync def handle_post_json(scope, receive, send):\n    \"\"\"Parse JSON body and return it.\"\"\"\n    if scope[\"method\"] != \"POST\":\n        await send_error(send, 405, \"Method Not Allowed\")\n        return\n\n    # Read request body\n    body_parts = []\n    while True:\n        message = await receive()\n        body = message.get(\"body\", b\"\")\n        if body:\n            body_parts.append(body)\n        if not message.get(\"more_body\", False):\n            break\n\n    request_body = b\"\".join(body_parts)\n\n    try:\n        data = json.loads(request_body.decode(\"utf-8\"))\n    except (json.JSONDecodeError, UnicodeDecodeError) as e:\n        await send_error(send, 400, f\"Invalid JSON: {e}\")\n        return\n\n    response = {\n        \"received\": data,\n        \"type\": type(data).__name__,\n    }\n    response_body = json.dumps(response).encode(\"utf-8\")\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 200,\n        \"headers\": [\n            (b\"content-type\", b\"application/json\"),\n            (b\"content-length\", str(len(response_body)).encode()),\n        ],\n    })\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": response_body,\n        \"more_body\": False,\n    })\n\n\nasync def handle_delay(scope, receive, send):\n    \"\"\"Respond after a delay (for timeout testing).\"\"\"\n    import asyncio\n\n    # Drain request body\n    await drain_body(receive)\n\n    # Parse delay from query string\n    query = scope[\"query_string\"].decode(\"latin-1\")\n    delay = 1.0  # Default 1 second\n\n    for param in query.split(\"&\"):\n        if param.startswith(\"seconds=\"):\n            try:\n                delay = float(param[8:])\n                # Limit to 30 seconds\n                delay = min(delay, 30.0)\n            except ValueError:\n                pass\n\n    await asyncio.sleep(delay)\n\n    body = json.dumps({\"delayed\": delay}).encode(\"utf-8\")\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 200,\n        \"headers\": [\n            (b\"content-type\", b\"application/json\"),\n            (b\"content-length\", str(len(body)).encode()),\n        ],\n    })\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": body,\n        \"more_body\": False,\n    })\n\n\nasync def handle_health(scope, receive, send):\n    \"\"\"Health check endpoint.\"\"\"\n    await drain_body(receive)\n\n    body = b\"OK\"\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 200,\n        \"headers\": [\n            (b\"content-type\", b\"text/plain\"),\n            (b\"content-length\", str(len(body)).encode()),\n        ],\n    })\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": body,\n        \"more_body\": False,\n    })\n\n\nasync def handle_early_hints(scope, receive, send):\n    \"\"\"Send 103 Early Hints before the response.\"\"\"\n    await drain_body(receive)\n\n    # Send 103 Early Hints\n    await send({\n        \"type\": \"http.response.informational\",\n        \"status\": 103,\n        \"headers\": [\n            (b\"link\", b\"</style.css>; rel=preload; as=style\"),\n            (b\"link\", b\"</script.js>; rel=preload; as=script\"),\n        ],\n    })\n\n    # Send actual response\n    body = b\"Response with Early Hints\"\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 200,\n        \"headers\": [\n            (b\"content-type\", b\"text/plain\"),\n            (b\"content-length\", str(len(body)).encode()),\n        ],\n    })\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": body,\n        \"more_body\": False,\n    })\n\n\nasync def handle_cookies(scope, receive, send):\n    \"\"\"Set and return cookies.\"\"\"\n    await drain_body(receive)\n\n    # Parse query for cookie values to set\n    query = scope[\"query_string\"].decode(\"latin-1\")\n    cookies_to_set = []\n\n    for param in query.split(\"&\"):\n        if param.startswith(\"set=\"):\n            cookie_value = param[4:]\n            cookies_to_set.append((b\"set-cookie\", cookie_value.encode()))\n\n    # Get existing cookies from request\n    request_cookies = {}\n    for name, value in scope[\"headers\"]:\n        if name == b\"cookie\":\n            cookie_str = value.decode(\"latin-1\")\n            for cookie in cookie_str.split(\";\"):\n                cookie = cookie.strip()\n                if \"=\" in cookie:\n                    k, v = cookie.split(\"=\", 1)\n                    request_cookies[k] = v\n\n    response = {\n        \"request_cookies\": request_cookies,\n        \"set_cookies\": [c[1].decode() for c in cookies_to_set],\n    }\n    body = json.dumps(response).encode(\"utf-8\")\n\n    headers = [\n        (b\"content-type\", b\"application/json\"),\n        (b\"content-length\", str(len(body)).encode()),\n    ]\n    headers.extend(cookies_to_set)\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 200,\n        \"headers\": headers,\n    })\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": body,\n        \"more_body\": False,\n    })\n\n\nasync def handle_redirect(scope, receive, send):\n    \"\"\"Redirect to another URL.\"\"\"\n    await drain_body(receive)\n\n    # Parse query for redirect target\n    query = scope[\"query_string\"].decode(\"latin-1\")\n    location = \"/\"\n    status = 302\n\n    for param in query.split(\"&\"):\n        if param.startswith(\"to=\"):\n            location = param[3:]\n        elif param.startswith(\"status=\"):\n            try:\n                status = int(param[7:])\n                if status not in (301, 302, 303, 307, 308):\n                    status = 302\n            except ValueError:\n                pass\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": status,\n        \"headers\": [\n            (b\"location\", location.encode()),\n            (b\"content-length\", b\"0\"),\n        ],\n    })\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": b\"\",\n        \"more_body\": False,\n    })\n\n\nasync def handle_not_found(scope, receive, send):\n    \"\"\"Handle 404 Not Found.\"\"\"\n    await drain_body(receive)\n    await send_error(send, 404, \"Not Found\")\n\n\nasync def drain_body(receive):\n    \"\"\"Drain the request body.\"\"\"\n    while True:\n        message = await receive()\n        if not message.get(\"more_body\", False):\n            break\n\n\nasync def send_error(send, status, message):\n    \"\"\"Send an error response.\"\"\"\n    body = message.encode(\"utf-8\")\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": status,\n        \"headers\": [\n            (b\"content-type\", b\"text/plain\"),\n            (b\"content-length\", str(len(body)).encode()),\n        ],\n    })\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": body,\n        \"more_body\": False,\n    })\n"
  },
  {
    "path": "tests/docker/asgi_compliance/apps/lifespan_app.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nLifespan test application for ASGI compliance testing.\n\nTests the ASGI lifespan protocol including startup, shutdown,\nand state sharing between lifespan and request handlers.\n\"\"\"\n\nimport json\nimport os\nimport time\n\n\n# Module-level state to track lifespan events (fallback if scope state unavailable)\n_lifespan_state = {\n    \"startup_called\": False,\n    \"startup_complete\": False,\n    \"shutdown_called\": False,\n    \"startup_time\": None,\n    \"startup_count\": 0,\n    \"request_count\": 0,\n}\n\n\nasync def app(scope, receive, send):\n    \"\"\"Main ASGI application with lifespan support.\"\"\"\n    if scope[\"type\"] == \"lifespan\":\n        await handle_lifespan(scope, receive, send)\n        return\n\n    if scope[\"type\"] != \"http\":\n        return\n\n    path = scope[\"path\"]\n\n    if path == \"/\":\n        await handle_root(scope, receive, send)\n    elif path == \"/state\":\n        await handle_state(scope, receive, send)\n    elif path == \"/lifespan-info\":\n        await handle_lifespan_info(scope, receive, send)\n    elif path == \"/counter\":\n        await handle_counter(scope, receive, send)\n    elif path == \"/health\":\n        await handle_health(scope, receive, send)\n    else:\n        await handle_not_found(scope, receive, send)\n\n\nasync def handle_lifespan(scope, receive, send):\n    \"\"\"Handle ASGI lifespan protocol.\"\"\"\n    global _lifespan_state\n\n    while True:\n        message = await receive()\n\n        if message[\"type\"] == \"lifespan.startup\":\n            _lifespan_state[\"startup_called\"] = True\n            _lifespan_state[\"startup_time\"] = time.time()\n            _lifespan_state[\"startup_count\"] += 1\n\n            # Check for failure trigger via environment\n            if os.environ.get(\"LIFESPAN_FAIL_STARTUP\") == \"1\":\n                await send({\n                    \"type\": \"lifespan.startup.failed\",\n                    \"message\": \"Startup failed (triggered by environment)\",\n                })\n                return\n\n            # Initialize state if available\n            if \"state\" in scope:\n                scope[\"state\"][\"lifespan_started\"] = True\n                scope[\"state\"][\"startup_time\"] = _lifespan_state[\"startup_time\"]\n                scope[\"state\"][\"db_connection\"] = \"simulated_connection\"\n                scope[\"state\"][\"cache\"] = {}\n                scope[\"state\"][\"request_count\"] = 0\n\n            _lifespan_state[\"startup_complete\"] = True\n\n            await send({\"type\": \"lifespan.startup.complete\"})\n\n        elif message[\"type\"] == \"lifespan.shutdown\":\n            _lifespan_state[\"shutdown_called\"] = True\n\n            # Cleanup state if available\n            if \"state\" in scope:\n                scope[\"state\"][\"lifespan_stopped\"] = True\n                scope[\"state\"][\"shutdown_time\"] = time.time()\n\n            await send({\"type\": \"lifespan.shutdown.complete\"})\n            return\n\n\nasync def handle_root(scope, receive, send):\n    \"\"\"Root endpoint.\"\"\"\n    await drain_body(receive)\n\n    _lifespan_state[\"request_count\"] += 1\n\n    # Increment request count in state if available\n    if \"state\" in scope and \"request_count\" in scope[\"state\"]:\n        scope[\"state\"][\"request_count\"] += 1\n\n    body = b\"Lifespan Test App\"\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 200,\n        \"headers\": [\n            (b\"content-type\", b\"text/plain\"),\n            (b\"content-length\", str(len(body)).encode()),\n        ],\n    })\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": body,\n        \"more_body\": False,\n    })\n\n\nasync def handle_state(scope, receive, send):\n    \"\"\"Return the current state (from scope or module-level).\"\"\"\n    await drain_body(receive)\n\n    _lifespan_state[\"request_count\"] += 1\n\n    # Collect state information\n    state_info = {\n        \"module_state\": {\n            \"startup_called\": _lifespan_state[\"startup_called\"],\n            \"startup_complete\": _lifespan_state[\"startup_complete\"],\n            \"shutdown_called\": _lifespan_state[\"shutdown_called\"],\n            \"startup_time\": _lifespan_state[\"startup_time\"],\n            \"startup_count\": _lifespan_state[\"startup_count\"],\n            \"request_count\": _lifespan_state[\"request_count\"],\n        },\n        \"scope_state_available\": \"state\" in scope,\n    }\n\n    if \"state\" in scope:\n        # Serialize scope state (only simple types)\n        scope_state = {}\n        for key, value in scope[\"state\"].items():\n            try:\n                json.dumps(value)  # Test if serializable\n                scope_state[key] = value\n            except (TypeError, ValueError):\n                scope_state[key] = str(type(value).__name__)\n\n        state_info[\"scope_state\"] = scope_state\n\n    body = json.dumps(state_info, indent=2, default=str).encode(\"utf-8\")\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 200,\n        \"headers\": [\n            (b\"content-type\", b\"application/json\"),\n            (b\"content-length\", str(len(body)).encode()),\n        ],\n    })\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": body,\n        \"more_body\": False,\n    })\n\n\nasync def handle_lifespan_info(scope, receive, send):\n    \"\"\"Return lifespan-specific information.\"\"\"\n    await drain_body(receive)\n\n    info = {\n        \"lifespan_supported\": True,\n        \"startup_complete\": _lifespan_state[\"startup_complete\"],\n        \"scope_state_present\": \"state\" in scope,\n        \"uptime_seconds\": None,\n    }\n\n    if _lifespan_state[\"startup_time\"]:\n        info[\"uptime_seconds\"] = time.time() - _lifespan_state[\"startup_time\"]\n\n    if \"state\" in scope:\n        info[\"state_keys\"] = list(scope[\"state\"].keys())\n        if \"db_connection\" in scope[\"state\"]:\n            info[\"db_connection_status\"] = \"active\"\n\n    body = json.dumps(info, indent=2).encode(\"utf-8\")\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 200,\n        \"headers\": [\n            (b\"content-type\", b\"application/json\"),\n            (b\"content-length\", str(len(body)).encode()),\n        ],\n    })\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": body,\n        \"more_body\": False,\n    })\n\n\nasync def handle_counter(scope, receive, send):\n    \"\"\"Increment and return a counter (tests state persistence).\"\"\"\n    await drain_body(receive)\n\n    _lifespan_state[\"request_count\"] += 1\n\n    counter_value = _lifespan_state[\"request_count\"]\n\n    # Also track in scope state if available\n    if \"state\" in scope:\n        scope[\"state\"][\"request_count\"] = scope[\"state\"].get(\"request_count\", 0) + 1\n        counter_value = scope[\"state\"][\"request_count\"]\n\n    body = json.dumps({\n        \"counter\": counter_value,\n        \"source\": \"scope_state\" if \"state\" in scope else \"module_state\",\n    }).encode(\"utf-8\")\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 200,\n        \"headers\": [\n            (b\"content-type\", b\"application/json\"),\n            (b\"content-length\", str(len(body)).encode()),\n        ],\n    })\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": body,\n        \"more_body\": False,\n    })\n\n\nasync def handle_health(scope, receive, send):\n    \"\"\"Health check that verifies lifespan startup completed.\"\"\"\n    await drain_body(receive)\n\n    if not _lifespan_state[\"startup_complete\"]:\n        body = b\"Lifespan not started\"\n        status = 503\n    else:\n        body = b\"OK\"\n        status = 200\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": status,\n        \"headers\": [\n            (b\"content-type\", b\"text/plain\"),\n            (b\"content-length\", str(len(body)).encode()),\n        ],\n    })\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": body,\n        \"more_body\": False,\n    })\n\n\nasync def handle_not_found(scope, receive, send):\n    \"\"\"Handle 404 Not Found.\"\"\"\n    await drain_body(receive)\n    await send_error(send, 404, \"Not Found\")\n\n\nasync def drain_body(receive):\n    \"\"\"Drain the request body.\"\"\"\n    while True:\n        message = await receive()\n        if not message.get(\"more_body\", False):\n            break\n\n\nasync def send_error(send, status, message):\n    \"\"\"Send an error response.\"\"\"\n    body = message.encode(\"utf-8\")\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": status,\n        \"headers\": [\n            (b\"content-type\", b\"text/plain\"),\n            (b\"content-length\", str(len(body)).encode()),\n        ],\n    })\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": body,\n        \"more_body\": False,\n    })\n\n\n# Application factory for explicit lifespan support\ndef create_app():\n    \"\"\"Create the ASGI application.\"\"\"\n    return app\n"
  },
  {
    "path": "tests/docker/asgi_compliance/apps/main_app.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nMain ASGI application for compliance testing.\n\nRoutes requests to appropriate test applications based on path prefix.\nThis is the primary entry point for Docker-based integration tests.\n\"\"\"\n\nimport json\nimport time\n\nfrom .http_app import app as http_app\nfrom .websocket_app import app as websocket_app\nfrom .streaming_app import app as streaming_app\nfrom .lifespan_app import app as lifespan_app\nfrom .framework_apps import combined_app as framework_app\n\n\n# Global state for lifespan\n_app_state = {\n    \"started\": False,\n    \"startup_time\": None,\n}\n\n\nasync def app(scope, receive, send):\n    \"\"\"Main routing application.\n\n    Routes based on path prefix:\n    - /http/* -> HTTP test endpoints\n    - /ws/* -> WebSocket test endpoints\n    - /stream/* -> Streaming test endpoints\n    - /lifespan/* -> Lifespan test endpoints\n    - /framework/* -> Framework integration tests\n    - / -> Root with info\n    - /health -> Health check\n    \"\"\"\n    if scope[\"type\"] == \"lifespan\":\n        await handle_lifespan(scope, receive, send)\n        return\n\n    path = scope.get(\"path\", \"\")\n\n    # WebSocket handling - check scope type\n    if scope[\"type\"] == \"websocket\":\n        if path.startswith(\"/ws/\"):\n            await websocket_app(scope, receive, send)\n        elif path.startswith(\"/framework/\"):\n            # Route to framework WebSocket handlers\n            new_scope = dict(scope)\n            new_scope[\"path\"] = path[10:] or \"/\"\n            new_scope[\"raw_path\"] = new_scope[\"path\"].encode(\"latin-1\")\n            await framework_app(new_scope, receive, send)\n        else:\n            await websocket_app(scope, receive, send)\n        return\n\n    # HTTP routing\n    if scope[\"type\"] == \"http\":\n        if path == \"/\" or path == \"\":\n            await handle_root(scope, receive, send)\n        elif path == \"/health\":\n            await handle_health(scope, receive, send)\n        elif path == \"/info\":\n            await handle_info(scope, receive, send)\n        elif path.startswith(\"/http/\"):\n            # Route to HTTP app, stripping prefix\n            new_scope = dict(scope)\n            new_scope[\"path\"] = path[5:] or \"/\"\n            new_scope[\"raw_path\"] = new_scope[\"path\"].encode(\"latin-1\")\n            await http_app(new_scope, receive, send)\n        elif path.startswith(\"/stream/\"):\n            # Route to streaming app, stripping prefix\n            new_scope = dict(scope)\n            new_scope[\"path\"] = path[7:] or \"/\"\n            new_scope[\"raw_path\"] = new_scope[\"path\"].encode(\"latin-1\")\n            await streaming_app(new_scope, receive, send)\n        elif path.startswith(\"/lifespan/\"):\n            # Route to lifespan app, stripping prefix\n            new_scope = dict(scope)\n            new_scope[\"path\"] = path[9:] or \"/\"\n            new_scope[\"raw_path\"] = new_scope[\"path\"].encode(\"latin-1\")\n            await lifespan_app(new_scope, receive, send)\n        elif path.startswith(\"/framework/\"):\n            # Route to framework app, stripping prefix\n            new_scope = dict(scope)\n            new_scope[\"path\"] = path[10:] or \"/\"\n            new_scope[\"raw_path\"] = new_scope[\"path\"].encode(\"latin-1\")\n            await framework_app(new_scope, receive, send)\n        else:\n            # Try direct routing to http_app for convenience\n            await http_app(scope, receive, send)\n\n\nasync def handle_lifespan(scope, receive, send):\n    \"\"\"Handle ASGI lifespan events.\"\"\"\n    global _app_state\n\n    while True:\n        message = await receive()\n\n        if message[\"type\"] == \"lifespan.startup\":\n            _app_state[\"started\"] = True\n            _app_state[\"startup_time\"] = time.time()\n\n            # Initialize state if available\n            if \"state\" in scope:\n                scope[\"state\"][\"main_app_started\"] = True\n                scope[\"state\"][\"startup_time\"] = _app_state[\"startup_time\"]\n\n            await send({\"type\": \"lifespan.startup.complete\"})\n\n        elif message[\"type\"] == \"lifespan.shutdown\":\n            _app_state[\"started\"] = False\n\n            if \"state\" in scope:\n                scope[\"state\"][\"main_app_started\"] = False\n\n            await send({\"type\": \"lifespan.shutdown.complete\"})\n            return\n\n\nasync def handle_root(scope, receive, send):\n    \"\"\"Root endpoint with routing information.\"\"\"\n    await drain_body(receive)\n\n    info = {\n        \"app\": \"ASGI Compliance Testbed\",\n        \"version\": \"1.0.0\",\n        \"routes\": {\n            \"/\": \"This info page\",\n            \"/health\": \"Health check endpoint\",\n            \"/info\": \"Detailed server info\",\n            \"/http/*\": \"HTTP test endpoints\",\n            \"/ws/*\": \"WebSocket test endpoints\",\n            \"/stream/*\": \"Streaming test endpoints\",\n            \"/lifespan/*\": \"Lifespan protocol tests\",\n            \"/framework/*\": \"Framework integration tests\",\n        },\n        \"http_endpoints\": [\n            \"/http/echo\", \"/http/headers\", \"/http/scope\",\n            \"/http/status?code=XXX\", \"/http/large\", \"/http/method\",\n            \"/http/query\", \"/http/post-json\", \"/http/delay\",\n            \"/http/early-hints\", \"/http/cookies\", \"/http/redirect\",\n        ],\n        \"websocket_endpoints\": [\n            \"/ws/echo\", \"/ws/echo-binary\", \"/ws/subprotocol\",\n            \"/ws/close?code=XXX\", \"/ws/scope\", \"/ws/reject\",\n            \"/ws/ping\", \"/ws/broadcast\", \"/ws/large\", \"/ws/delay\",\n        ],\n        \"streaming_endpoints\": [\n            \"/stream/streaming\", \"/stream/sse\", \"/stream/chunked\",\n            \"/stream/slow-stream\", \"/stream/large-stream\",\n            \"/stream/ndjson\", \"/stream/echo-stream\",\n        ],\n        \"lifespan_endpoints\": [\n            \"/lifespan/state\", \"/lifespan/lifespan-info\",\n            \"/lifespan/counter\", \"/lifespan/health\",\n        ],\n        \"framework_endpoints\": [\n            \"/framework/starlette/*\", \"/framework/fastapi/*\",\n        ],\n    }\n\n    body = json.dumps(info, indent=2).encode(\"utf-8\")\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 200,\n        \"headers\": [\n            (b\"content-type\", b\"application/json\"),\n            (b\"content-length\", str(len(body)).encode()),\n        ],\n    })\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": body,\n        \"more_body\": False,\n    })\n\n\nasync def handle_health(scope, receive, send):\n    \"\"\"Health check endpoint.\"\"\"\n    await drain_body(receive)\n\n    body = b\"OK\"\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 200,\n        \"headers\": [\n            (b\"content-type\", b\"text/plain\"),\n            (b\"content-length\", str(len(body)).encode()),\n        ],\n    })\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": body,\n        \"more_body\": False,\n    })\n\n\nasync def handle_info(scope, receive, send):\n    \"\"\"Detailed server information.\"\"\"\n    await drain_body(receive)\n\n    info = {\n        \"started\": _app_state[\"started\"],\n        \"startup_time\": _app_state[\"startup_time\"],\n        \"uptime\": time.time() - _app_state[\"startup_time\"] if _app_state[\"startup_time\"] else None,\n        \"scope_state_available\": \"state\" in scope,\n        \"asgi\": scope.get(\"asgi\", {}),\n        \"server\": list(scope[\"server\"]) if scope.get(\"server\") else None,\n    }\n\n    if \"state\" in scope:\n        info[\"state_keys\"] = list(scope[\"state\"].keys())\n\n    body = json.dumps(info, indent=2).encode(\"utf-8\")\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 200,\n        \"headers\": [\n            (b\"content-type\", b\"application/json\"),\n            (b\"content-length\", str(len(body)).encode()),\n        ],\n    })\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": body,\n        \"more_body\": False,\n    })\n\n\nasync def drain_body(receive):\n    \"\"\"Drain the request body.\"\"\"\n    while True:\n        message = await receive()\n        if not message.get(\"more_body\", False):\n            break\n"
  },
  {
    "path": "tests/docker/asgi_compliance/apps/streaming_app.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nStreaming test application for ASGI compliance testing.\n\nProvides endpoints for testing chunked transfer encoding,\nServer-Sent Events (SSE), and streaming responses.\n\"\"\"\n\nimport asyncio\nimport json\nimport time\n\n\nasync def app(scope, receive, send):\n    \"\"\"Main ASGI streaming application.\"\"\"\n    if scope[\"type\"] == \"lifespan\":\n        await handle_lifespan(scope, receive, send)\n        return\n\n    if scope[\"type\"] != \"http\":\n        return\n\n    path = scope[\"path\"]\n\n    # Route to appropriate handler\n    if path == \"/streaming\":\n        await handle_streaming(scope, receive, send)\n    elif path == \"/sse\":\n        await handle_sse(scope, receive, send)\n    elif path == \"/chunked\":\n        await handle_chunked(scope, receive, send)\n    elif path == \"/slow-stream\":\n        await handle_slow_stream(scope, receive, send)\n    elif path == \"/large-stream\":\n        await handle_large_stream(scope, receive, send)\n    elif path == \"/infinite\":\n        await handle_infinite(scope, receive, send)\n    elif path == \"/echo-stream\":\n        await handle_echo_stream(scope, receive, send)\n    elif path == \"/ndjson\":\n        await handle_ndjson(scope, receive, send)\n    elif path == \"/health\":\n        await handle_health(scope, receive, send)\n    else:\n        await handle_not_found(scope, receive, send)\n\n\nasync def handle_lifespan(scope, receive, send):\n    \"\"\"Handle ASGI lifespan events.\"\"\"\n    while True:\n        message = await receive()\n        if message[\"type\"] == \"lifespan.startup\":\n            await send({\"type\": \"lifespan.startup.complete\"})\n        elif message[\"type\"] == \"lifespan.shutdown\":\n            await send({\"type\": \"lifespan.shutdown.complete\"})\n            return\n\n\nasync def handle_streaming(scope, receive, send):\n    \"\"\"Basic streaming response without Content-Length.\"\"\"\n    await drain_body(receive)\n\n    # Parse chunk count from query\n    query = scope[\"query_string\"].decode(\"latin-1\")\n    chunks = 5\n\n    for param in query.split(\"&\"):\n        if param.startswith(\"chunks=\"):\n            try:\n                chunks = int(param[7:])\n                chunks = min(chunks, 100)\n            except ValueError:\n                pass\n\n    # Start response without Content-Length (triggers chunked encoding)\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 200,\n        \"headers\": [\n            (b\"content-type\", b\"text/plain\"),\n            # No content-length - server should use chunked encoding\n        ],\n    })\n\n    # Send chunks\n    for i in range(chunks):\n        chunk = f\"Chunk {i + 1} of {chunks}\\n\".encode(\"utf-8\")\n        await send({\n            \"type\": \"http.response.body\",\n            \"body\": chunk,\n            \"more_body\": i < chunks - 1,\n        })\n\n    # Final empty body to signal end (if not already done)\n    if chunks == 0:\n        await send({\n            \"type\": \"http.response.body\",\n            \"body\": b\"\",\n            \"more_body\": False,\n        })\n\n\nasync def handle_sse(scope, receive, send):\n    \"\"\"Server-Sent Events stream.\"\"\"\n    await drain_body(receive)\n\n    # Parse event count from query\n    query = scope[\"query_string\"].decode(\"latin-1\")\n    events = 5\n    delay = 0.5\n\n    for param in query.split(\"&\"):\n        if param.startswith(\"events=\"):\n            try:\n                events = int(param[7:])\n                events = min(events, 100)\n            except ValueError:\n                pass\n        elif param.startswith(\"delay=\"):\n            try:\n                delay = float(param[6:])\n                delay = min(delay, 5.0)\n            except ValueError:\n                pass\n\n    # SSE response headers\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 200,\n        \"headers\": [\n            (b\"content-type\", b\"text/event-stream\"),\n            (b\"cache-control\", b\"no-cache\"),\n            (b\"connection\", b\"keep-alive\"),\n            (b\"x-accel-buffering\", b\"no\"),  # Disable nginx buffering\n        ],\n    })\n\n    # Send SSE events\n    for i in range(events):\n        event_data = {\n            \"id\": i + 1,\n            \"total\": events,\n            \"timestamp\": time.time(),\n        }\n\n        # Format as SSE\n        sse_message = f\"id: {i + 1}\\nevent: message\\ndata: {json.dumps(event_data)}\\n\\n\"\n\n        await send({\n            \"type\": \"http.response.body\",\n            \"body\": sse_message.encode(\"utf-8\"),\n            \"more_body\": i < events - 1,\n        })\n\n        if i < events - 1:\n            await asyncio.sleep(delay)\n\n    # Send final empty body if needed\n    if events == 0:\n        await send({\n            \"type\": \"http.response.body\",\n            \"body\": b\"\",\n            \"more_body\": False,\n        })\n\n\nasync def handle_chunked(scope, receive, send):\n    \"\"\"Explicit chunked response with variable chunk sizes.\"\"\"\n    await drain_body(receive)\n\n    # Parse parameters from query\n    query = scope[\"query_string\"].decode(\"latin-1\")\n    chunk_sizes = [100, 500, 1000, 50, 200]  # Default varied sizes\n\n    for param in query.split(\"&\"):\n        if param.startswith(\"sizes=\"):\n            try:\n                sizes_str = param[6:]\n                chunk_sizes = [int(s) for s in sizes_str.split(\",\")]\n                chunk_sizes = [min(s, 100000) for s in chunk_sizes]  # 100KB max per chunk\n            except ValueError:\n                pass\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 200,\n        \"headers\": [\n            (b\"content-type\", b\"application/octet-stream\"),\n        ],\n    })\n\n    # Send chunks of specified sizes\n    for i, size in enumerate(chunk_sizes):\n        chunk = bytes([i % 256] * size)\n        await send({\n            \"type\": \"http.response.body\",\n            \"body\": chunk,\n            \"more_body\": i < len(chunk_sizes) - 1,\n        })\n\n\nasync def handle_slow_stream(scope, receive, send):\n    \"\"\"Slow streaming response with configurable delays.\"\"\"\n    await drain_body(receive)\n\n    query = scope[\"query_string\"].decode(\"latin-1\")\n    chunks = 10\n    delay = 0.5\n\n    for param in query.split(\"&\"):\n        if param.startswith(\"chunks=\"):\n            try:\n                chunks = int(param[7:])\n                chunks = min(chunks, 50)\n            except ValueError:\n                pass\n        elif param.startswith(\"delay=\"):\n            try:\n                delay = float(param[6:])\n                delay = min(delay, 5.0)\n            except ValueError:\n                pass\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 200,\n        \"headers\": [\n            (b\"content-type\", b\"text/plain\"),\n        ],\n    })\n\n    for i in range(chunks):\n        timestamp = time.time()\n        chunk = f\"[{timestamp:.3f}] Slow chunk {i + 1}/{chunks}\\n\".encode(\"utf-8\")\n\n        await send({\n            \"type\": \"http.response.body\",\n            \"body\": chunk,\n            \"more_body\": i < chunks - 1,\n        })\n\n        if i < chunks - 1:\n            await asyncio.sleep(delay)\n\n\nasync def handle_large_stream(scope, receive, send):\n    \"\"\"Stream a large response in chunks.\"\"\"\n    await drain_body(receive)\n\n    query = scope[\"query_string\"].decode(\"latin-1\")\n    total_size = 1024 * 1024  # 1MB default\n    chunk_size = 64 * 1024  # 64KB chunks\n\n    for param in query.split(\"&\"):\n        if param.startswith(\"size=\"):\n            try:\n                total_size = int(param[5:])\n                total_size = min(total_size, 100 * 1024 * 1024)  # 100MB max\n            except ValueError:\n                pass\n        elif param.startswith(\"chunk=\"):\n            try:\n                chunk_size = int(param[6:])\n                chunk_size = min(chunk_size, 1024 * 1024)  # 1MB max chunk\n            except ValueError:\n                pass\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 200,\n        \"headers\": [\n            (b\"content-type\", b\"application/octet-stream\"),\n        ],\n    })\n\n    sent = 0\n    while sent < total_size:\n        remaining = total_size - sent\n        current_chunk_size = min(chunk_size, remaining)\n        chunk = b\"x\" * current_chunk_size\n        sent += current_chunk_size\n\n        await send({\n            \"type\": \"http.response.body\",\n            \"body\": chunk,\n            \"more_body\": sent < total_size,\n        })\n\n\nasync def handle_infinite(scope, receive, send):\n    \"\"\"Infinite stream (until client disconnects or limit reached).\"\"\"\n    await drain_body(receive)\n\n    query = scope[\"query_string\"].decode(\"latin-1\")\n    max_chunks = 1000  # Safety limit\n    delay = 0.1\n\n    for param in query.split(\"&\"):\n        if param.startswith(\"max=\"):\n            try:\n                max_chunks = int(param[4:])\n                max_chunks = min(max_chunks, 10000)\n            except ValueError:\n                pass\n        elif param.startswith(\"delay=\"):\n            try:\n                delay = float(param[6:])\n                delay = max(delay, 0.01)  # Min 10ms\n            except ValueError:\n                pass\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 200,\n        \"headers\": [\n            (b\"content-type\", b\"text/plain\"),\n        ],\n    })\n\n    try:\n        for i in range(max_chunks):\n            chunk = f\"Infinite stream chunk {i + 1}\\n\".encode(\"utf-8\")\n\n            await send({\n                \"type\": \"http.response.body\",\n                \"body\": chunk,\n                \"more_body\": i < max_chunks - 1,\n            })\n\n            if i < max_chunks - 1:\n                await asyncio.sleep(delay)\n    except Exception:\n        # Client disconnected\n        pass\n\n\nasync def handle_echo_stream(scope, receive, send):\n    \"\"\"Echo request body as a stream.\"\"\"\n    # Start response immediately\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 200,\n        \"headers\": [\n            (b\"content-type\", b\"application/octet-stream\"),\n        ],\n    })\n\n    # Stream request body to response\n    chunk_count = 0\n    while True:\n        message = await receive()\n        body = message.get(\"body\", b\"\")\n        more_body = message.get(\"more_body\", False)\n\n        if body:\n            chunk_count += 1\n            # Add chunk info prefix\n            prefix = f\"[chunk {chunk_count}]: \".encode(\"utf-8\")\n            await send({\n                \"type\": \"http.response.body\",\n                \"body\": prefix + body + b\"\\n\",\n                \"more_body\": True,\n            })\n\n        if not more_body:\n            break\n\n    # Final chunk with summary\n    summary = f\"Total chunks received: {chunk_count}\\n\".encode(\"utf-8\")\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": summary,\n        \"more_body\": False,\n    })\n\n\nasync def handle_ndjson(scope, receive, send):\n    \"\"\"Newline-delimited JSON stream.\"\"\"\n    await drain_body(receive)\n\n    query = scope[\"query_string\"].decode(\"latin-1\")\n    records = 10\n    delay = 0.2\n\n    for param in query.split(\"&\"):\n        if param.startswith(\"records=\"):\n            try:\n                records = int(param[8:])\n                records = min(records, 1000)\n            except ValueError:\n                pass\n        elif param.startswith(\"delay=\"):\n            try:\n                delay = float(param[6:])\n                delay = min(delay, 5.0)\n            except ValueError:\n                pass\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 200,\n        \"headers\": [\n            (b\"content-type\", b\"application/x-ndjson\"),\n        ],\n    })\n\n    for i in range(records):\n        record = {\n            \"id\": i + 1,\n            \"timestamp\": time.time(),\n            \"data\": f\"Record {i + 1}\",\n        }\n\n        line = json.dumps(record) + \"\\n\"\n\n        await send({\n            \"type\": \"http.response.body\",\n            \"body\": line.encode(\"utf-8\"),\n            \"more_body\": i < records - 1,\n        })\n\n        if i < records - 1 and delay > 0:\n            await asyncio.sleep(delay)\n\n\nasync def handle_health(scope, receive, send):\n    \"\"\"Health check endpoint.\"\"\"\n    await drain_body(receive)\n\n    body = b\"OK\"\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 200,\n        \"headers\": [\n            (b\"content-type\", b\"text/plain\"),\n            (b\"content-length\", str(len(body)).encode()),\n        ],\n    })\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": body,\n        \"more_body\": False,\n    })\n\n\nasync def handle_not_found(scope, receive, send):\n    \"\"\"Handle 404 Not Found.\"\"\"\n    await drain_body(receive)\n\n    body = b\"Not Found\"\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 404,\n        \"headers\": [\n            (b\"content-type\", b\"text/plain\"),\n            (b\"content-length\", str(len(body)).encode()),\n        ],\n    })\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": body,\n        \"more_body\": False,\n    })\n\n\nasync def drain_body(receive):\n    \"\"\"Drain the request body.\"\"\"\n    while True:\n        message = await receive()\n        if not message.get(\"more_body\", False):\n            break\n"
  },
  {
    "path": "tests/docker/asgi_compliance/apps/websocket_app.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nWebSocket test application for ASGI compliance testing.\n\nProvides various WebSocket endpoints to test RFC 6455 compliance,\nmessage handling, and protocol features.\n\"\"\"\n\nimport json\n\n\nasync def app(scope, receive, send):\n    \"\"\"Main ASGI WebSocket application with multiple test endpoints.\"\"\"\n    if scope[\"type\"] == \"lifespan\":\n        await handle_lifespan(scope, receive, send)\n        return\n\n    if scope[\"type\"] != \"websocket\":\n        # Return 404 for non-WebSocket requests\n        if scope[\"type\"] == \"http\":\n            await send_http_error(send, 404, \"WebSocket endpoints only\")\n        return\n\n    path = scope[\"path\"]\n\n    # Route to appropriate handler\n    if path == \"/ws/echo\":\n        await handle_echo(scope, receive, send)\n    elif path == \"/ws/echo-binary\":\n        await handle_echo_binary(scope, receive, send)\n    elif path == \"/ws/subprotocol\":\n        await handle_subprotocol(scope, receive, send)\n    elif path.startswith(\"/ws/close\"):\n        await handle_close(scope, receive, send)\n    elif path == \"/ws/scope\":\n        await handle_scope(scope, receive, send)\n    elif path == \"/ws/reject\":\n        await handle_reject(scope, receive, send)\n    elif path == \"/ws/ping\":\n        await handle_ping(scope, receive, send)\n    elif path == \"/ws/broadcast\":\n        await handle_broadcast(scope, receive, send)\n    elif path == \"/ws/large\":\n        await handle_large_message(scope, receive, send)\n    elif path == \"/ws/fragmented\":\n        await handle_fragmented(scope, receive, send)\n    elif path == \"/ws/delay\":\n        await handle_delay(scope, receive, send)\n    else:\n        # Accept but immediately close for unknown paths\n        await handle_unknown(scope, receive, send)\n\n\nasync def handle_lifespan(scope, receive, send):\n    \"\"\"Handle ASGI lifespan events.\"\"\"\n    while True:\n        message = await receive()\n        if message[\"type\"] == \"lifespan.startup\":\n            await send({\"type\": \"lifespan.startup.complete\"})\n        elif message[\"type\"] == \"lifespan.shutdown\":\n            await send({\"type\": \"lifespan.shutdown.complete\"})\n            return\n\n\nasync def handle_echo(scope, receive, send):\n    \"\"\"Echo text messages back to the client.\"\"\"\n    # Wait for connection\n    message = await receive()\n    if message[\"type\"] != \"websocket.connect\":\n        return\n\n    # Accept the connection\n    await send({\"type\": \"websocket.accept\"})\n\n    # Echo messages until disconnect\n    while True:\n        message = await receive()\n\n        if message[\"type\"] == \"websocket.receive\":\n            # Echo back text messages\n            if \"text\" in message:\n                await send({\n                    \"type\": \"websocket.send\",\n                    \"text\": message[\"text\"],\n                })\n            elif \"bytes\" in message:\n                # Convert binary to text for echo\n                await send({\n                    \"type\": \"websocket.send\",\n                    \"text\": message[\"bytes\"].decode(\"utf-8\", errors=\"replace\"),\n                })\n\n        elif message[\"type\"] == \"websocket.disconnect\":\n            break\n\n\nasync def handle_echo_binary(scope, receive, send):\n    \"\"\"Echo binary messages back to the client.\"\"\"\n    message = await receive()\n    if message[\"type\"] != \"websocket.connect\":\n        return\n\n    await send({\"type\": \"websocket.accept\"})\n\n    while True:\n        message = await receive()\n\n        if message[\"type\"] == \"websocket.receive\":\n            if \"bytes\" in message:\n                await send({\n                    \"type\": \"websocket.send\",\n                    \"bytes\": message[\"bytes\"],\n                })\n            elif \"text\" in message:\n                # Convert text to binary for echo\n                await send({\n                    \"type\": \"websocket.send\",\n                    \"bytes\": message[\"text\"].encode(\"utf-8\"),\n                })\n\n        elif message[\"type\"] == \"websocket.disconnect\":\n            break\n\n\nasync def handle_subprotocol(scope, receive, send):\n    \"\"\"Negotiate WebSocket subprotocol.\"\"\"\n    message = await receive()\n    if message[\"type\"] != \"websocket.connect\":\n        return\n\n    # Get requested subprotocols\n    requested = scope.get(\"subprotocols\", [])\n\n    # Prefer graphql-ws, then json, then first available\n    selected = None\n    preferred = [\"graphql-ws\", \"json\", \"wamp\"]\n\n    for proto in preferred:\n        if proto in requested:\n            selected = proto\n            break\n\n    if not selected and requested:\n        selected = requested[0]\n\n    # Accept with selected subprotocol\n    accept_msg = {\"type\": \"websocket.accept\"}\n    if selected:\n        accept_msg[\"subprotocol\"] = selected\n\n    await send(accept_msg)\n\n    # Send confirmation message\n    response = {\n        \"requested\": requested,\n        \"selected\": selected,\n    }\n    await send({\n        \"type\": \"websocket.send\",\n        \"text\": json.dumps(response),\n    })\n\n    # Wait for disconnect\n    while True:\n        message = await receive()\n        if message[\"type\"] == \"websocket.disconnect\":\n            break\n\n\nasync def handle_close(scope, receive, send):\n    \"\"\"Close connection with specific code from query parameter.\"\"\"\n    message = await receive()\n    if message[\"type\"] != \"websocket.connect\":\n        return\n\n    await send({\"type\": \"websocket.accept\"})\n\n    # Parse close code from query string\n    query = scope[\"query_string\"].decode(\"latin-1\")\n    close_code = 1000  # Normal closure\n    close_reason = \"\"\n\n    for param in query.split(\"&\"):\n        if param.startswith(\"code=\"):\n            try:\n                close_code = int(param[5:])\n            except ValueError:\n                pass\n        elif param.startswith(\"reason=\"):\n            close_reason = param[7:]\n\n    # Send close with specified code\n    close_msg = {\n        \"type\": \"websocket.close\",\n        \"code\": close_code,\n    }\n    if close_reason:\n        close_msg[\"reason\"] = close_reason\n\n    await send(close_msg)\n\n\nasync def handle_scope(scope, receive, send):\n    \"\"\"Return WebSocket scope as JSON.\"\"\"\n    message = await receive()\n    if message[\"type\"] != \"websocket.connect\":\n        return\n\n    await send({\"type\": \"websocket.accept\"})\n\n    # Create JSON-serializable scope\n    scope_json = {\n        \"type\": scope[\"type\"],\n        \"asgi\": scope[\"asgi\"],\n        \"http_version\": scope[\"http_version\"],\n        \"scheme\": scope[\"scheme\"],\n        \"path\": scope[\"path\"],\n        \"raw_path\": scope[\"raw_path\"].decode(\"latin-1\") if scope.get(\"raw_path\") else None,\n        \"query_string\": scope[\"query_string\"].decode(\"latin-1\") if scope.get(\"query_string\") else \"\",\n        \"root_path\": scope.get(\"root_path\", \"\"),\n        \"headers\": [\n            [name.decode(\"latin-1\"), value.decode(\"latin-1\")]\n            for name, value in scope[\"headers\"]\n        ],\n        \"server\": list(scope[\"server\"]) if scope.get(\"server\") else None,\n        \"client\": list(scope[\"client\"]) if scope.get(\"client\") else None,\n        \"subprotocols\": scope.get(\"subprotocols\", []),\n    }\n\n    await send({\n        \"type\": \"websocket.send\",\n        \"text\": json.dumps(scope_json, indent=2),\n    })\n\n    # Wait for disconnect\n    while True:\n        message = await receive()\n        if message[\"type\"] == \"websocket.disconnect\":\n            break\n\n\nasync def handle_reject(scope, receive, send):\n    \"\"\"Reject the WebSocket connection.\"\"\"\n    message = await receive()\n    if message[\"type\"] != \"websocket.connect\":\n        return\n\n    # Close without accepting - this rejects the connection\n    await send({\n        \"type\": \"websocket.close\",\n        \"code\": 1008,  # Policy violation\n        \"reason\": \"Connection rejected\",\n    })\n\n\nasync def handle_ping(scope, receive, send):\n    \"\"\"Echo ping messages (handled at protocol level, but test app behavior).\"\"\"\n    message = await receive()\n    if message[\"type\"] != \"websocket.connect\":\n        return\n\n    await send({\"type\": \"websocket.accept\"})\n\n    # Send a message indicating ping/pong is handled at protocol level\n    await send({\n        \"type\": \"websocket.send\",\n        \"text\": json.dumps({\n            \"info\": \"Ping/pong is handled at the protocol level\",\n            \"note\": \"Send any message to test echo\",\n        }),\n    })\n\n    while True:\n        message = await receive()\n\n        if message[\"type\"] == \"websocket.receive\":\n            # Echo back\n            if \"text\" in message:\n                await send({\"type\": \"websocket.send\", \"text\": message[\"text\"]})\n            elif \"bytes\" in message:\n                await send({\"type\": \"websocket.send\", \"bytes\": message[\"bytes\"]})\n\n        elif message[\"type\"] == \"websocket.disconnect\":\n            break\n\n\nasync def handle_broadcast(scope, receive, send):\n    \"\"\"Simple broadcast simulation - echo message multiple times.\"\"\"\n    message = await receive()\n    if message[\"type\"] != \"websocket.connect\":\n        return\n\n    await send({\"type\": \"websocket.accept\"})\n\n    # Parse broadcast count from query\n    query = scope[\"query_string\"].decode(\"latin-1\")\n    count = 3  # Default\n\n    for param in query.split(\"&\"):\n        if param.startswith(\"count=\"):\n            try:\n                count = int(param[6:])\n                count = min(count, 100)  # Limit\n            except ValueError:\n                pass\n\n    while True:\n        message = await receive()\n\n        if message[\"type\"] == \"websocket.receive\":\n            text = message.get(\"text\", \"\")\n\n            # \"Broadcast\" by sending multiple copies\n            for i in range(count):\n                await send({\n                    \"type\": \"websocket.send\",\n                    \"text\": json.dumps({\n                        \"copy\": i + 1,\n                        \"of\": count,\n                        \"message\": text,\n                    }),\n                })\n\n        elif message[\"type\"] == \"websocket.disconnect\":\n            break\n\n\nasync def handle_large_message(scope, receive, send):\n    \"\"\"Test large message handling.\"\"\"\n    message = await receive()\n    if message[\"type\"] != \"websocket.connect\":\n        return\n\n    await send({\"type\": \"websocket.accept\"})\n\n    # Parse size from query\n    query = scope[\"query_string\"].decode(\"latin-1\")\n    size = 64 * 1024  # 64KB default\n\n    for param in query.split(\"&\"):\n        if param.startswith(\"size=\"):\n            try:\n                size = int(param[5:])\n                size = min(size, 1024 * 1024)  # 1MB limit\n            except ValueError:\n                pass\n\n    # Send large message\n    large_data = \"x\" * size\n    await send({\n        \"type\": \"websocket.send\",\n        \"text\": large_data,\n    })\n\n    # Echo any received messages\n    while True:\n        message = await receive()\n\n        if message[\"type\"] == \"websocket.receive\":\n            if \"text\" in message:\n                response = {\n                    \"received_length\": len(message[\"text\"]),\n                    \"sent_length\": size,\n                }\n                await send({\n                    \"type\": \"websocket.send\",\n                    \"text\": json.dumps(response),\n                })\n\n        elif message[\"type\"] == \"websocket.disconnect\":\n            break\n\n\nasync def handle_fragmented(scope, receive, send):\n    \"\"\"Test fragmented message handling (assembled by protocol).\"\"\"\n    message = await receive()\n    if message[\"type\"] != \"websocket.connect\":\n        return\n\n    await send({\"type\": \"websocket.accept\"})\n\n    await send({\n        \"type\": \"websocket.send\",\n        \"text\": json.dumps({\n            \"info\": \"Fragmented frames are assembled at protocol level\",\n            \"note\": \"This app receives complete messages\",\n        }),\n    })\n\n    # Echo messages with length info\n    while True:\n        message = await receive()\n\n        if message[\"type\"] == \"websocket.receive\":\n            if \"text\" in message:\n                await send({\n                    \"type\": \"websocket.send\",\n                    \"text\": json.dumps({\n                        \"received\": message[\"text\"],\n                        \"length\": len(message[\"text\"]),\n                        \"type\": \"text\",\n                    }),\n                })\n            elif \"bytes\" in message:\n                await send({\n                    \"type\": \"websocket.send\",\n                    \"text\": json.dumps({\n                        \"length\": len(message[\"bytes\"]),\n                        \"type\": \"binary\",\n                    }),\n                })\n\n        elif message[\"type\"] == \"websocket.disconnect\":\n            break\n\n\nasync def handle_delay(scope, receive, send):\n    \"\"\"Test delayed responses.\"\"\"\n    import asyncio\n\n    message = await receive()\n    if message[\"type\"] != \"websocket.connect\":\n        return\n\n    await send({\"type\": \"websocket.accept\"})\n\n    # Parse delay from query\n    query = scope[\"query_string\"].decode(\"latin-1\")\n    delay = 1.0\n\n    for param in query.split(\"&\"):\n        if param.startswith(\"seconds=\"):\n            try:\n                delay = float(param[8:])\n                delay = min(delay, 30.0)  # 30s limit\n            except ValueError:\n                pass\n\n    while True:\n        message = await receive()\n\n        if message[\"type\"] == \"websocket.receive\":\n            await asyncio.sleep(delay)\n            if \"text\" in message:\n                await send({\n                    \"type\": \"websocket.send\",\n                    \"text\": json.dumps({\n                        \"delayed_by\": delay,\n                        \"message\": message[\"text\"],\n                    }),\n                })\n\n        elif message[\"type\"] == \"websocket.disconnect\":\n            break\n\n\nasync def handle_unknown(scope, receive, send):\n    \"\"\"Handle unknown WebSocket paths - accept then close.\"\"\"\n    message = await receive()\n    if message[\"type\"] != \"websocket.connect\":\n        return\n\n    await send({\"type\": \"websocket.accept\"})\n    await send({\n        \"type\": \"websocket.send\",\n        \"text\": json.dumps({\n            \"error\": \"Unknown path\",\n            \"path\": scope[\"path\"],\n        }),\n    })\n    await send({\n        \"type\": \"websocket.close\",\n        \"code\": 1000,\n    })\n\n\nasync def send_http_error(send, status, message):\n    \"\"\"Send HTTP error response (for non-WebSocket requests).\"\"\"\n    body = message.encode(\"utf-8\")\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": status,\n        \"headers\": [\n            (b\"content-type\", b\"text/plain\"),\n            (b\"content-length\", str(len(body)).encode()),\n        ],\n    })\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": body,\n        \"more_body\": False,\n    })\n"
  },
  {
    "path": "tests/docker/asgi_compliance/certs/server.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDkzCCAnugAwIBAgIUHdlZ9co55+sdCalL7KLszmMTEzgwDQYJKoZIhvcNAQEL\nBQAwOTESMBAGA1UEAwwJbG9jYWxob3N0MRYwFAYDVQQKDA1HdW5pY29ybiBUZXN0\nMQswCQYDVQQGEwJVUzAeFw0yNjAyMDIxMDIwMDRaFw0yNjAyMDMxMDIwMDRaMDkx\nEjAQBgNVBAMMCWxvY2FsaG9zdDEWMBQGA1UECgwNR3VuaWNvcm4gVGVzdDELMAkG\nA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDS07vGRH7C\ni7LBJp5fn+i/vQoaE7y9MVqTN4SH1iSJgUti6fAYBQkCGsC1X0QDHaffsH17p5zV\nDY6pNEdpOfM9cbIhtWl078jTsSsuHRnBg2g3zcyaXNN7voruKoAgrN5gTBpY1yAx\niW7s431EwEBd4MGQm++FOn83Dw2uAa5Xfdf4HMo4EDwAfVLir89th63L9q3rxGGY\nt+C1XzQ54t2EnHpOycnDkgAlRogRC8Js+14eVwSZcsWTqEHLp9lal74BTRpY9GiS\nmktm4p71IBqqB1dnIByii2kBNuCzJDhAFdLqjLv81iZirfZx0pGfcvR6iARCLKLA\nOOcB7jz5rycLAgMBAAGjgZIwgY8wHQYDVR0OBBYEFN8wH1VLJd6rbI53UgHM2xSD\ne99DMB8GA1UdIwQYMBaAFN8wH1VLJd6rbI53UgHM2xSDe99DMA8GA1UdEwEB/wQF\nMAMBAf8wPAYDVR0RBDUwM4IJbG9jYWxob3N0gg1ndW5pY29ybi1hc2dpghFndW5p\nY29ybi1hc2dpLXNzbIcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEAMe8So/3/bGe7\nn/xoeij6BZrX3O1hTNy3iUeAxuhyLS9o00Z7B9swgwiPnHz3/2JnxXzZH5XXX5XI\nDbT36LY2CzPERYkmWmo5w2JZ8wneN/J/LuLF5djpjwM+ItLZlDNUnZoETqWmsur1\nY0e+G3lUN9dc3XchOq7ONqmoWGNDzlO/LGytnLBhsw5v4mnKeDSwPeD2CdAQ8Cl0\nzcdYOibetAG4nLsrDvFYPxYNtQGNsAKji/Wg1pc9WtbSBFennW0T9pFKuYBAavdQ\nKHzlYBexBiGNWWu5XlXpA7YMFm2Na8m3C4A/oxIiJL3lc1i+GlxQ2cTKNIPekwbH\nfjKuvNNfcw==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/docker/asgi_compliance/certs/server.key",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDS07vGRH7Ci7LB\nJp5fn+i/vQoaE7y9MVqTN4SH1iSJgUti6fAYBQkCGsC1X0QDHaffsH17p5zVDY6p\nNEdpOfM9cbIhtWl078jTsSsuHRnBg2g3zcyaXNN7voruKoAgrN5gTBpY1yAxiW7s\n431EwEBd4MGQm++FOn83Dw2uAa5Xfdf4HMo4EDwAfVLir89th63L9q3rxGGYt+C1\nXzQ54t2EnHpOycnDkgAlRogRC8Js+14eVwSZcsWTqEHLp9lal74BTRpY9GiSmktm\n4p71IBqqB1dnIByii2kBNuCzJDhAFdLqjLv81iZirfZx0pGfcvR6iARCLKLAOOcB\n7jz5rycLAgMBAAECggEABuX6s1NM1BzLrcCpVsOquZHIuzwa5Ud9VgSioR2dEHEn\nzR+OeM3uLFPa9/q00c1Hz7mJdMeLo16c/mUn7DkczM19k9cvXi4oyhR2YnBKH9tA\ne7yPwOh5dKEYqdy/vuuPhmMdfmHiCDAd1KgU7AnGXjJoFjOkALeYFgfq3Xi2Naw1\nqMqWnCjKoR/0WmCqozrQ4KAh8GD105D7bB69kP4qwNz5HYbfWLFI4naY6EPmc4wV\ncoadcK2GKjGQSWc+EAmimc7nVdogR2RrA3TGwEc+dAUn8oYHWfz8uK9SRZpNQTI4\nS5sqNQL5UsrM0NYcSFW6eg0bhBl0YmqyHr7bfdL+EQKBgQDrxNUtgPR5oDEQTO8p\nrwpzVbdfeJBmtr4Aw0LZn5DAEX8LxrbwESK9jnClEHCoCosgWZuYrJ+c5716Rlid\nvdKQkqOfpf+4JW88oMTjuzYR0wFJ5rHC3+OctCzjgYrfanhALzhQKqiHhCrcjWPZ\nxm0lxz1oMKQoDNoT7ab+UhhRHQKBgQDk6wA/CBJ/JqOQ2+wTifqpK65PlTDaA5e4\nqEQrQ66kOhVpdTaDOMgtdwvsBSQ1t5CL3b8ytO9gGRBBXVlri8518F5fJrlRRBDX\nTP5hkJXOw/gpJAiCie7dPpChu7nDkq6JxmzMEYw5wf1AIzYwarauNRWDVPyaU/nD\nrJY/GTrIRwKBgAuF1DFkIw6qsJsuV2X/IxCd+NdWqiALAGBDKso+DTIF6OKndJtp\nCvyesIywsADWexQ6rOsaTLa7cLxAIeabt2XPdOXBlCzoz3X0GYtTxAG9AUweVUPD\n83jeKW95DlN6/aONa0AnxZLR99JNqrqjAwScpzinX+6BKktdCxNU6dFVAoGAPIwD\nlqhV7BeWL5xbhpd6GwCYrCfzsdY9bPPkg+T07i8GtsvvzSlZmNzh5F0/xI12x+ew\nyIKexbYbXI6KNi3WP8+Bxn0BiwMLyUZuCfQqC3Q90PPc5FoDObVwn7Z9XcMQMxSu\ndhM2GZi7mRk3Hfs7sjwMIp556X/Ikf62Bp5vs8UCgYEA1nGfXK6DpMGlnEDno3X2\ncWHV1MgDE6ojR0GHMsQQvuQVj/cHNgCJmDEBtTlq7/cM7HPPmNSCBteHfDQQ7UXy\nViEQgo6p9NOByr73zmxlhGEirHE/hUmF8qOHYBvjgPU+jVEKF5yRiz0T3sC5Z3bQ\nAhTGjfXfHsH7SvdrQQNl4DE=\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "tests/docker/asgi_compliance/conftest.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Pytest fixtures for ASGI compliance Docker integration tests.\"\"\"\n\nimport subprocess\nimport time\nimport socket\nfrom pathlib import Path\n\nimport pytest\n\n# Directory containing this conftest.py\nDOCKER_DIR = Path(__file__).parent\nCERTS_DIR = DOCKER_DIR / \"certs\"\n\n\ndef generate_self_signed_cert(certs_dir: Path) -> None:\n    \"\"\"Generate self-signed SSL certificates for testing.\"\"\"\n    certs_dir.mkdir(parents=True, exist_ok=True)\n    cert_file = certs_dir / \"server.crt\"\n    key_file = certs_dir / \"server.key\"\n\n    # Skip if certs already exist and are recent (less than 1 day old)\n    if cert_file.exists() and key_file.exists():\n        age = time.time() - cert_file.stat().st_mtime\n        if age < 86400:  # 1 day\n            return\n\n    # Generate self-signed certificate\n    subprocess.run(\n        [\n            \"openssl\", \"req\", \"-x509\", \"-newkey\", \"rsa:2048\",\n            \"-keyout\", str(key_file),\n            \"-out\", str(cert_file),\n            \"-days\", \"1\",\n            \"-nodes\",\n            \"-subj\", \"/CN=localhost/O=Gunicorn Test/C=US\",\n            \"-addext\", \"subjectAltName=DNS:localhost,DNS:gunicorn-asgi,DNS:gunicorn-asgi-ssl,IP:127.0.0.1\"\n        ],\n        check=True,\n        capture_output=True\n    )\n    # Set readable permissions\n    cert_file.chmod(0o644)\n    key_file.chmod(0o644)\n\n\ndef wait_for_http_service(host: str, port: int, timeout: int = 60) -> bool:\n    \"\"\"Wait for an HTTP service to become available.\"\"\"\n    start_time = time.time()\n    while time.time() - start_time < timeout:\n        try:\n            with socket.create_connection((host, port), timeout=5):\n                return True\n        except (socket.error, OSError):\n            time.sleep(1)\n    return False\n\n\ndef wait_for_https_service(host: str, port: int, timeout: int = 60) -> bool:\n    \"\"\"Wait for an HTTPS service to become available.\"\"\"\n    import ssl\n\n    start_time = time.time()\n    while time.time() - start_time < timeout:\n        try:\n            ctx = ssl.create_default_context()\n            ctx.check_hostname = False\n            ctx.verify_mode = ssl.CERT_NONE\n\n            with socket.create_connection((host, port), timeout=5) as sock:\n                with ctx.wrap_socket(sock, server_hostname=host):\n                    return True\n        except (socket.error, ssl.SSLError, OSError):\n            time.sleep(1)\n    return False\n\n\n@pytest.fixture(scope=\"session\")\ndef docker_compose_file():\n    \"\"\"Return the path to docker-compose.yml.\"\"\"\n    return DOCKER_DIR / \"docker-compose.yml\"\n\n\n@pytest.fixture(scope=\"session\")\ndef certs_dir():\n    \"\"\"Generate and return the certs directory.\"\"\"\n    generate_self_signed_cert(CERTS_DIR)\n    return CERTS_DIR\n\n\n@pytest.fixture(scope=\"session\")\ndef docker_services(docker_compose_file, certs_dir):\n    \"\"\"Start Docker services for the test session.\"\"\"\n    compose_file = str(docker_compose_file)\n\n    # Check if Docker is available\n    try:\n        subprocess.run(\n            [\"docker\", \"info\"],\n            check=True,\n            capture_output=True\n        )\n    except (subprocess.CalledProcessError, FileNotFoundError):\n        pytest.skip(\"Docker is not available\")\n\n    # Check if docker compose is available\n    try:\n        subprocess.run(\n            [\"docker\", \"compose\", \"version\"],\n            check=True,\n            capture_output=True\n        )\n    except subprocess.CalledProcessError:\n        pytest.skip(\"Docker Compose is not available\")\n\n    # Build and start services\n    try:\n        subprocess.run(\n            [\"docker\", \"compose\", \"-f\", compose_file, \"build\"],\n            check=True,\n            cwd=DOCKER_DIR\n        )\n        subprocess.run(\n            [\"docker\", \"compose\", \"-f\", compose_file, \"up\", \"-d\"],\n            check=True,\n            cwd=DOCKER_DIR\n        )\n\n        # Wait for services to be healthy\n        gunicorn_http_ready = wait_for_http_service(\"127.0.0.1\", 8000, timeout=60)\n        gunicorn_https_ready = wait_for_https_service(\"127.0.0.1\", 8445, timeout=60)\n        nginx_http_ready = wait_for_http_service(\"127.0.0.1\", 8080, timeout=60)\n        nginx_https_ready = wait_for_https_service(\"127.0.0.1\", 8444, timeout=60)\n\n        if not gunicorn_http_ready:\n            result = subprocess.run(\n                [\"docker\", \"compose\", \"-f\", compose_file, \"logs\", \"gunicorn-asgi\"],\n                capture_output=True,\n                text=True,\n                cwd=DOCKER_DIR\n            )\n            pytest.fail(f\"Gunicorn HTTP service failed to start. Logs:\\n{result.stdout}\\n{result.stderr}\")\n\n        if not gunicorn_https_ready:\n            result = subprocess.run(\n                [\"docker\", \"compose\", \"-f\", compose_file, \"logs\", \"gunicorn-asgi-ssl\"],\n                capture_output=True,\n                text=True,\n                cwd=DOCKER_DIR\n            )\n            pytest.fail(f\"Gunicorn HTTPS service failed to start. Logs:\\n{result.stdout}\\n{result.stderr}\")\n\n        if not nginx_http_ready or not nginx_https_ready:\n            result = subprocess.run(\n                [\"docker\", \"compose\", \"-f\", compose_file, \"logs\", \"nginx-proxy\"],\n                capture_output=True,\n                text=True,\n                cwd=DOCKER_DIR\n            )\n            pytest.fail(f\"Nginx service failed to start. Logs:\\n{result.stdout}\\n{result.stderr}\")\n\n        yield {\n            \"gunicorn_http\": \"http://127.0.0.1:8000\",\n            \"gunicorn_https\": \"https://127.0.0.1:8445\",\n            \"nginx_http\": \"http://127.0.0.1:8080\",\n            \"nginx_https\": \"https://127.0.0.1:8444\",\n        }\n\n    finally:\n        # Stop and remove services\n        subprocess.run(\n            [\"docker\", \"compose\", \"-f\", compose_file, \"down\", \"-v\", \"--remove-orphans\"],\n            cwd=DOCKER_DIR,\n            capture_output=True\n        )\n\n\n# ============================================================================\n# URL Fixtures\n# ============================================================================\n\n@pytest.fixture\ndef gunicorn_url(docker_services):\n    \"\"\"Return the gunicorn HTTP service URL.\"\"\"\n    return docker_services[\"gunicorn_http\"]\n\n\n@pytest.fixture\ndef gunicorn_ssl_url(docker_services):\n    \"\"\"Return the gunicorn HTTPS service URL.\"\"\"\n    return docker_services[\"gunicorn_https\"]\n\n\n@pytest.fixture\ndef nginx_url(docker_services):\n    \"\"\"Return the nginx HTTP proxy URL.\"\"\"\n    return docker_services[\"nginx_http\"]\n\n\n@pytest.fixture\ndef nginx_ssl_url(docker_services):\n    \"\"\"Return the nginx HTTPS proxy URL.\"\"\"\n    return docker_services[\"nginx_https\"]\n\n\n# ============================================================================\n# HTTP Client Fixtures\n# ============================================================================\n\n@pytest.fixture\ndef http_client():\n    \"\"\"Create a standard HTTP client.\"\"\"\n    httpx = pytest.importorskip(\"httpx\")\n    client = httpx.Client(verify=False, timeout=30.0, follow_redirects=False)\n    yield client\n    client.close()\n\n\n@pytest.fixture\ndef http2_client():\n    \"\"\"Create an HTTP/2 capable client.\"\"\"\n    httpx = pytest.importorskip(\"httpx\")\n    client = httpx.Client(http2=True, verify=False, timeout=30.0)\n    yield client\n    client.close()\n\n\n@pytest.fixture\nasync def async_http_client():\n    \"\"\"Create an async HTTP client.\"\"\"\n    httpx = pytest.importorskip(\"httpx\")\n    async with httpx.AsyncClient(verify=False, timeout=30.0) as client:\n        yield client\n\n\n@pytest.fixture\ndef async_http_client_factory():\n    \"\"\"Factory for creating async HTTP clients.\"\"\"\n    httpx = pytest.importorskip(\"httpx\")\n\n    async def create_client(**kwargs):\n        defaults = {\"verify\": False, \"timeout\": 30.0}\n        defaults.update(kwargs)\n        return httpx.AsyncClient(**defaults)\n\n    return create_client\n\n\n# ============================================================================\n# WebSocket Client Fixtures\n# ============================================================================\n\n@pytest.fixture\ndef websocket_connect():\n    \"\"\"Factory for creating WebSocket connections.\"\"\"\n    websockets = pytest.importorskip(\"websockets\")\n\n    async def connect(url, **kwargs):\n        \"\"\"Connect to a WebSocket endpoint.\n\n        Args:\n            url: WebSocket URL (ws:// or wss://)\n            **kwargs: Additional arguments for websockets.connect()\n\n        Returns:\n            WebSocket connection\n        \"\"\"\n        import ssl\n\n        # Default SSL context for wss://\n        if url.startswith(\"wss://\") and \"ssl\" not in kwargs:\n            ssl_context = ssl.create_default_context()\n            ssl_context.check_hostname = False\n            ssl_context.verify_mode = ssl.CERT_NONE\n            kwargs[\"ssl\"] = ssl_context\n\n        return await websockets.connect(url, **kwargs)\n\n    return connect\n\n\n# ============================================================================\n# Streaming Client Fixtures\n# ============================================================================\n\n@pytest.fixture\ndef sse_client():\n    \"\"\"Create a client for Server-Sent Events.\"\"\"\n    httpx = pytest.importorskip(\"httpx\")\n\n    class SSEClient:\n        def __init__(self):\n            self.client = httpx.Client(verify=False, timeout=60.0)\n\n        def stream(self, url):\n            \"\"\"Stream SSE events from URL.\"\"\"\n            with self.client.stream(\"GET\", url, headers={\"Accept\": \"text/event-stream\"}) as response:\n                buffer = \"\"\n                for chunk in response.iter_text():\n                    buffer += chunk\n                    while \"\\n\\n\" in buffer:\n                        event, buffer = buffer.split(\"\\n\\n\", 1)\n                        yield self._parse_event(event)\n\n        def _parse_event(self, event_text):\n            \"\"\"Parse an SSE event.\"\"\"\n            event = {\"data\": None, \"event\": None, \"id\": None}\n            for line in event_text.strip().split(\"\\n\"):\n                if line.startswith(\"data: \"):\n                    event[\"data\"] = line[6:]\n                elif line.startswith(\"event: \"):\n                    event[\"event\"] = line[7:]\n                elif line.startswith(\"id: \"):\n                    event[\"id\"] = line[4:]\n            return event\n\n        def close(self):\n            self.client.close()\n\n    client = SSEClient()\n    yield client\n    client.close()\n\n\n@pytest.fixture\ndef streaming_client():\n    \"\"\"Create a client for chunked/streaming responses.\"\"\"\n    httpx = pytest.importorskip(\"httpx\")\n\n    class StreamingClient:\n        def __init__(self):\n            self.client = httpx.Client(verify=False, timeout=60.0)\n\n        def stream_chunks(self, url, method=\"GET\", **kwargs):\n            \"\"\"Stream response chunks from URL.\"\"\"\n            with self.client.stream(method, url, **kwargs) as response:\n                for chunk in response.iter_bytes():\n                    if chunk:\n                        yield chunk\n\n        def stream_lines(self, url, method=\"GET\", **kwargs):\n            \"\"\"Stream response lines from URL.\"\"\"\n            with self.client.stream(method, url, **kwargs) as response:\n                for line in response.iter_lines():\n                    yield line\n\n        def close(self):\n            self.client.close()\n\n    client = StreamingClient()\n    yield client\n    client.close()\n\n\n# ============================================================================\n# Test Markers\n# ============================================================================\n\ndef pytest_configure(config):\n    \"\"\"Configure custom pytest markers.\"\"\"\n    config.addinivalue_line(\"markers\", \"docker: tests requiring Docker\")\n    config.addinivalue_line(\"markers\", \"asgi: ASGI-related tests\")\n    config.addinivalue_line(\"markers\", \"websocket: WebSocket tests\")\n    config.addinivalue_line(\"markers\", \"streaming: Streaming response tests\")\n    config.addinivalue_line(\"markers\", \"lifespan: Lifespan protocol tests\")\n    config.addinivalue_line(\"markers\", \"framework: Framework integration tests\")\n    config.addinivalue_line(\"markers\", \"concurrency: Concurrency tests\")\n    config.addinivalue_line(\"markers\", \"http2: HTTP/2 specific tests\")\n    config.addinivalue_line(\"markers\", \"ssl: SSL/TLS tests\")\n    config.addinivalue_line(\"markers\", \"integration: Integration tests\")\n"
  },
  {
    "path": "tests/docker/asgi_compliance/docker-compose.yml",
    "content": "services:\n  gunicorn-asgi:\n    build:\n      context: ../../../\n      dockerfile: tests/docker/asgi_compliance/Dockerfile.gunicorn\n    ports:\n      - \"8000:8000\"   # HTTP\n      - \"8443:8443\"   # HTTPS\n    volumes:\n      - ./certs:/certs:ro\n      - ./apps:/app/apps:ro\n    environment:\n      - GUNICORN_CERTFILE=/certs/server.crt\n      - GUNICORN_KEYFILE=/certs/server.key\n    healthcheck:\n      test: [\"CMD\", \"python\", \"-c\", \"import socket; s=socket.socket(); s.settimeout(2); s.connect(('localhost',8000)); s.close()\"]\n      interval: 2s\n      timeout: 5s\n      retries: 15\n      start_period: 5s\n\n  gunicorn-asgi-ssl:\n    build:\n      context: ../../../\n      dockerfile: tests/docker/asgi_compliance/Dockerfile.gunicorn\n    ports:\n      - \"8445:8443\"\n    volumes:\n      - ./certs:/certs:ro\n      - ./apps:/app/apps:ro\n    environment:\n      - GUNICORN_CERTFILE=/certs/server.crt\n      - GUNICORN_KEYFILE=/certs/server.key\n      - USE_SSL=1\n    healthcheck:\n      test: [\"CMD\", \"python\", \"-c\", \"import ssl,socket; s=socket.socket(); s.settimeout(2); ctx=ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE; ss=ctx.wrap_socket(s,server_hostname='localhost'); ss.connect(('localhost',8443)); ss.close()\"]\n      interval: 2s\n      timeout: 5s\n      retries: 15\n      start_period: 5s\n\n  nginx-proxy:\n    build:\n      context: .\n      dockerfile: Dockerfile.nginx\n    ports:\n      - \"8080:8080\"   # HTTP proxy\n      - \"8444:8444\"   # HTTPS proxy\n    volumes:\n      - ./certs:/certs:ro\n      - ./nginx.conf:/etc/nginx/nginx.conf:ro\n    depends_on:\n      gunicorn-asgi:\n        condition: service_healthy\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:8080/health\"]\n      interval: 2s\n      timeout: 5s\n      retries: 15\n      start_period: 5s\n\nnetworks:\n  default:\n    driver: bridge\n"
  },
  {
    "path": "tests/docker/asgi_compliance/nginx.conf",
    "content": "worker_processes auto;\nerror_log /var/log/nginx/error.log warn;\npid /var/run/nginx.pid;\n\nevents {\n    worker_connections 1024;\n}\n\nhttp {\n    include /etc/nginx/mime.types;\n    default_type application/octet-stream;\n\n    # Use Docker DNS resolver, IPv4 only to avoid IPv6 connection issues\n    resolver 127.0.0.11 ipv6=off valid=10s;\n\n    log_format main '$remote_addr - $remote_user [$time_local] \"$request\" '\n                    '$status $body_bytes_sent \"$http_referer\" '\n                    '\"$http_user_agent\" \"$http_x_forwarded_for\"';\n\n    access_log /var/log/nginx/access.log main;\n\n    sendfile on;\n    keepalive_timeout 65;\n\n    # Map for WebSocket upgrade - use empty string for non-WebSocket to enable keepalive\n    map $http_upgrade $connection_upgrade {\n        default upgrade;\n        '' '';\n    }\n\n    upstream gunicorn_asgi {\n        server gunicorn-asgi:8000 max_fails=0;\n        keepalive 32;\n    }\n\n    upstream gunicorn_asgi_ssl {\n        server gunicorn-asgi-ssl:8443 max_fails=0;\n        keepalive 32;\n    }\n\n    # HTTP server (port 8080)\n    server {\n        listen 8080;\n        server_name localhost;\n\n        # Increase body size limit for large request tests\n        client_max_body_size 100m;\n\n        # Health check endpoint\n        location /health {\n            return 200 'OK';\n            add_header Content-Type text/plain;\n        }\n\n        # WebSocket locations\n        location /ws/ {\n            proxy_pass http://gunicorn_asgi;\n            proxy_http_version 1.1;\n\n            # WebSocket upgrade headers\n            proxy_set_header Upgrade $http_upgrade;\n            proxy_set_header Connection $connection_upgrade;\n\n            # Standard proxy headers\n            proxy_set_header Host $host;\n            proxy_set_header X-Real-IP $remote_addr;\n            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n            proxy_set_header X-Forwarded-Proto $scheme;\n\n            # WebSocket timeouts\n            proxy_connect_timeout 60s;\n            proxy_send_timeout 300s;\n            proxy_read_timeout 300s;\n        }\n\n        # Streaming locations - disable buffering\n        location /stream/ {\n            proxy_pass http://gunicorn_asgi;\n            proxy_http_version 1.1;\n\n            # Disable buffering for streaming\n            proxy_buffering off;\n            proxy_cache off;\n\n            # SSE specific\n            proxy_set_header Connection '';\n            chunked_transfer_encoding on;\n\n            # Headers\n            proxy_set_header Host $host;\n            proxy_set_header X-Real-IP $remote_addr;\n            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n            proxy_set_header X-Forwarded-Proto $scheme;\n            proxy_set_header X-Accel-Buffering no;\n\n            # Longer timeouts for streaming\n            proxy_connect_timeout 60s;\n            proxy_send_timeout 300s;\n            proxy_read_timeout 300s;\n        }\n\n        # Default location\n        location / {\n            proxy_pass http://gunicorn_asgi;\n            proxy_http_version 1.1;\n\n            # Retry on connection errors\n            proxy_next_upstream error timeout http_502;\n            proxy_next_upstream_tries 2;\n\n            # Headers\n            proxy_set_header Host $host;\n            proxy_set_header X-Real-IP $remote_addr;\n            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n            proxy_set_header X-Forwarded-Proto $scheme;\n            proxy_set_header X-Forwarded-Host $host;\n            proxy_set_header X-Forwarded-Port $server_port;\n\n            # Support WebSocket upgrade if requested\n            proxy_set_header Upgrade $http_upgrade;\n            proxy_set_header Connection $connection_upgrade;\n\n            # Buffering settings\n            proxy_buffering on;\n            proxy_buffer_size 4k;\n            proxy_buffers 8 4k;\n\n            # Timeouts\n            proxy_connect_timeout 60s;\n            proxy_send_timeout 60s;\n            proxy_read_timeout 60s;\n        }\n    }\n\n    # HTTPS server (port 8444)\n    server {\n        listen 8444 ssl;\n        http2 on;\n        server_name localhost;\n\n        ssl_certificate /certs/server.crt;\n        ssl_certificate_key /certs/server.key;\n        ssl_protocols TLSv1.2 TLSv1.3;\n        ssl_ciphers HIGH:!aNULL:!MD5;\n        ssl_prefer_server_ciphers on;\n\n        # HTTP/2 settings\n        http2_max_concurrent_streams 128;\n\n        # Increase body size limit\n        client_max_body_size 100m;\n\n        # Health check endpoint\n        location /health {\n            return 200 'OK';\n            add_header Content-Type text/plain;\n        }\n\n        # WebSocket locations (over HTTPS)\n        location /ws/ {\n            proxy_pass http://gunicorn_asgi;\n            proxy_http_version 1.1;\n\n            # WebSocket upgrade headers\n            proxy_set_header Upgrade $http_upgrade;\n            proxy_set_header Connection $connection_upgrade;\n\n            # Standard proxy headers\n            proxy_set_header Host $host;\n            proxy_set_header X-Real-IP $remote_addr;\n            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n            proxy_set_header X-Forwarded-Proto $scheme;\n\n            # WebSocket timeouts\n            proxy_connect_timeout 60s;\n            proxy_send_timeout 300s;\n            proxy_read_timeout 300s;\n        }\n\n        # Streaming locations - disable buffering\n        location /stream/ {\n            proxy_pass http://gunicorn_asgi;\n            proxy_http_version 1.1;\n\n            # Disable buffering for streaming\n            proxy_buffering off;\n            proxy_cache off;\n\n            # SSE specific\n            proxy_set_header Connection '';\n            chunked_transfer_encoding on;\n\n            # Headers\n            proxy_set_header Host $host;\n            proxy_set_header X-Real-IP $remote_addr;\n            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n            proxy_set_header X-Forwarded-Proto $scheme;\n            proxy_set_header X-Accel-Buffering no;\n\n            # Longer timeouts for streaming\n            proxy_connect_timeout 60s;\n            proxy_send_timeout 300s;\n            proxy_read_timeout 300s;\n        }\n\n        # Default location\n        location / {\n            proxy_pass http://gunicorn_asgi;\n            proxy_http_version 1.1;\n\n            # Retry on connection errors\n            proxy_next_upstream error timeout http_502;\n            proxy_next_upstream_tries 2;\n\n            # Headers\n            proxy_set_header Host $host;\n            proxy_set_header X-Real-IP $remote_addr;\n            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n            proxy_set_header X-Forwarded-Proto $scheme;\n            proxy_set_header X-Forwarded-Host $host;\n            proxy_set_header X-Forwarded-Port $server_port;\n\n            # Support WebSocket upgrade if requested\n            proxy_set_header Upgrade $http_upgrade;\n            proxy_set_header Connection $connection_upgrade;\n\n            # Buffering settings\n            proxy_buffering on;\n            proxy_buffer_size 4k;\n            proxy_buffers 8 4k;\n\n            # Timeouts\n            proxy_connect_timeout 60s;\n            proxy_send_timeout 60s;\n            proxy_read_timeout 60s;\n        }\n    }\n}\n"
  },
  {
    "path": "tests/docker/asgi_compliance/test_concurrency.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nConcurrency integration tests for ASGI.\n\nTests concurrent connections, mixed protocols, and load handling.\n\"\"\"\n\nimport asyncio\nimport json\n\nimport pytest\n\npytestmark = [\n    pytest.mark.docker,\n    pytest.mark.asgi,\n    pytest.mark.concurrency,\n    pytest.mark.integration,\n]\n\n\n# ============================================================================\n# Concurrent HTTP Requests\n# ============================================================================\n\n@pytest.mark.asyncio\nclass TestConcurrentHTTP:\n    \"\"\"Test concurrent HTTP request handling.\"\"\"\n\n    async def test_concurrent_simple_requests(self, async_http_client_factory, gunicorn_url):\n        \"\"\"Test many concurrent simple requests.\"\"\"\n        async with await async_http_client_factory() as client:\n            async def make_request(i):\n                response = await client.get(f\"{gunicorn_url}/http/\")\n                return response.status_code, i\n\n            tasks = [make_request(i) for i in range(50)]\n            results = await asyncio.gather(*tasks)\n\n            # All should succeed\n            assert all(status == 200 for status, _ in results)\n\n    async def test_concurrent_echo_requests(self, async_http_client_factory, gunicorn_url):\n        \"\"\"Test concurrent echo requests with unique data.\"\"\"\n        async with await async_http_client_factory() as client:\n            async def echo_request(i):\n                data = f\"request_{i}\"\n                response = await client.post(\n                    f\"{gunicorn_url}/http/echo\",\n                    content=data.encode()\n                )\n                return response.text == data, i\n\n            tasks = [echo_request(i) for i in range(30)]\n            results = await asyncio.gather(*tasks)\n\n            # All should echo correctly\n            assert all(success for success, _ in results)\n\n    async def test_concurrent_different_endpoints(self, async_http_client_factory, gunicorn_url):\n        \"\"\"Test concurrent requests to different endpoints.\"\"\"\n        async with await async_http_client_factory() as client:\n            async def get_root():\n                return await client.get(f\"{gunicorn_url}/http/\")\n\n            async def get_headers():\n                return await client.get(f\"{gunicorn_url}/http/headers\")\n\n            async def get_scope():\n                return await client.get(f\"{gunicorn_url}/http/scope\")\n\n            async def get_health():\n                return await client.get(f\"{gunicorn_url}/http/health\")\n\n            # Mix of different endpoints\n            tasks = [\n                get_root(), get_headers(), get_scope(), get_health(),\n                get_root(), get_headers(), get_scope(), get_health(),\n                get_root(), get_headers(), get_scope(), get_health(),\n            ]\n\n            results = await asyncio.gather(*tasks)\n            assert all(r.status_code == 200 for r in results)\n\n    async def test_concurrent_with_delays(self, async_http_client_factory, gunicorn_url):\n        \"\"\"Test concurrent requests with varying delays.\"\"\"\n        async with await async_http_client_factory(timeout=30.0) as client:\n            async def delayed_request(delay_ms):\n                response = await client.get(\n                    f\"{gunicorn_url}/http/delay?ms={delay_ms}\"\n                )\n                return response.status_code == 200\n\n            # Various delays\n            delays = [100, 200, 50, 150, 100, 200, 50]\n            tasks = [delayed_request(d) for d in delays]\n            results = await asyncio.gather(*tasks)\n\n            assert all(results)\n\n\n# ============================================================================\n# Concurrent WebSocket Connections\n# ============================================================================\n\n@pytest.mark.asyncio\nclass TestConcurrentWebSocket:\n    \"\"\"Test concurrent WebSocket connections.\"\"\"\n\n    async def test_many_concurrent_websockets(self, websocket_connect, gunicorn_url):\n        \"\"\"Test many concurrent WebSocket connections.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/echo\"\n\n        async def ws_echo(i):\n            async with await websocket_connect(ws_url) as ws:\n                message = f\"concurrent_{i}\"\n                await ws.send(message)\n                response = await ws.recv()\n                return response == message\n\n        tasks = [ws_echo(i) for i in range(20)]\n        results = await asyncio.gather(*tasks)\n\n        assert all(results)\n\n    async def test_concurrent_websocket_many_messages(self, websocket_connect, gunicorn_url):\n        \"\"\"Test concurrent WebSocket connections with many messages each.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/echo\"\n\n        async def ws_multiple_messages(conn_id):\n            async with await websocket_connect(ws_url) as ws:\n                for i in range(10):\n                    message = f\"conn_{conn_id}_msg_{i}\"\n                    await ws.send(message)\n                    response = await ws.recv()\n                    if response != message:\n                        return False\n                return True\n\n        tasks = [ws_multiple_messages(i) for i in range(10)]\n        results = await asyncio.gather(*tasks)\n\n        assert all(results)\n\n\n# ============================================================================\n# Mixed Protocol Tests\n# ============================================================================\n\n@pytest.mark.asyncio\nclass TestMixedProtocols:\n    \"\"\"Test mixed HTTP and WebSocket concurrent access.\"\"\"\n\n    async def test_http_and_websocket_concurrent(\n        self, async_http_client_factory, websocket_connect, gunicorn_url\n    ):\n        \"\"\"Test concurrent HTTP and WebSocket requests.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/echo\"\n\n        async def http_request(client):\n            response = await client.get(f\"{gunicorn_url}/http/\")\n            return response.status_code == 200\n\n        async def websocket_echo():\n            async with await websocket_connect(ws_url) as ws:\n                await ws.send(\"mixed\")\n                response = await ws.recv()\n                return response == \"mixed\"\n\n        async with await async_http_client_factory() as client:\n            # Interleaved HTTP and WebSocket tasks\n            tasks = [\n                http_request(client),\n                websocket_echo(),\n                http_request(client),\n                websocket_echo(),\n                http_request(client),\n                websocket_echo(),\n            ]\n\n            results = await asyncio.gather(*tasks)\n            assert all(results)\n\n    async def test_streaming_and_http_concurrent(\n        self, async_http_client_factory, gunicorn_url\n    ):\n        \"\"\"Test concurrent streaming and regular HTTP requests.\"\"\"\n        async with await async_http_client_factory(timeout=60.0) as client:\n            async def regular_request():\n                response = await client.get(f\"{gunicorn_url}/http/\")\n                return response.status_code == 200\n\n            async def streaming_request():\n                async with client.stream(\n                    \"GET\",\n                    f\"{gunicorn_url}/stream/streaming?chunks=5\"\n                ) as response:\n                    chunks = []\n                    async for chunk in response.aiter_bytes():\n                        chunks.append(chunk)\n                    return len(chunks) > 0\n\n            tasks = [\n                regular_request(),\n                streaming_request(),\n                regular_request(),\n                streaming_request(),\n                regular_request(),\n            ]\n\n            results = await asyncio.gather(*tasks)\n            assert all(results)\n\n\n# ============================================================================\n# Connection Reuse Tests\n# ============================================================================\n\n@pytest.mark.asyncio\nclass TestConnectionReuse:\n    \"\"\"Test connection reuse and keep-alive.\"\"\"\n\n    async def test_many_requests_single_client(\n        self, async_http_client_factory, gunicorn_url\n    ):\n        \"\"\"Test many sequential requests on single client.\"\"\"\n        async with await async_http_client_factory() as client:\n            for i in range(100):\n                response = await client.get(f\"{gunicorn_url}/http/?iter={i}\")\n                assert response.status_code == 200\n\n    async def test_keep_alive_stress(self, async_http_client_factory, gunicorn_url):\n        \"\"\"Test keep-alive under stress.\"\"\"\n        async with await async_http_client_factory() as client:\n            # Rapid sequential requests\n            for _ in range(50):\n                tasks = [\n                    client.get(f\"{gunicorn_url}/http/\"),\n                    client.get(f\"{gunicorn_url}/http/headers\"),\n                ]\n                results = await asyncio.gather(*tasks)\n                assert all(r.status_code == 200 for r in results)\n\n\n# ============================================================================\n# Load Tests\n# ============================================================================\n\n@pytest.mark.asyncio\nclass TestLoad:\n    \"\"\"Test load handling.\"\"\"\n\n    async def test_burst_requests(self, async_http_client_factory, gunicorn_url):\n        \"\"\"Test handling burst of requests.\"\"\"\n        async with await async_http_client_factory() as client:\n            async def burst():\n                tasks = [\n                    client.get(f\"{gunicorn_url}/http/\")\n                    for _ in range(100)\n                ]\n                return await asyncio.gather(*tasks, return_exceptions=True)\n\n            results = await burst()\n\n            # Count successful responses\n            success = sum(\n                1 for r in results\n                if not isinstance(r, Exception) and r.status_code == 200\n            )\n\n            # Most should succeed (allow for some failures under load)\n            assert success >= 90, f\"Only {success}/100 requests succeeded\"\n\n    async def test_sustained_load(self, async_http_client_factory, gunicorn_url):\n        \"\"\"Test sustained load over time.\"\"\"\n        async with await async_http_client_factory() as client:\n            success_count = 0\n            total = 0\n\n            # 5 iterations of 20 concurrent requests\n            for _ in range(5):\n                tasks = [\n                    client.get(f\"{gunicorn_url}/http/\")\n                    for _ in range(20)\n                ]\n                results = await asyncio.gather(*tasks, return_exceptions=True)\n\n                for r in results:\n                    total += 1\n                    if not isinstance(r, Exception) and r.status_code == 200:\n                        success_count += 1\n\n                # Small delay between batches\n                await asyncio.sleep(0.1)\n\n            # High success rate expected\n            assert success_count / total >= 0.95\n\n\n# ============================================================================\n# Resource Exhaustion Tests\n# ============================================================================\n\n@pytest.mark.asyncio\nclass TestResourceHandling:\n    \"\"\"Test handling of resource constraints.\"\"\"\n\n    async def test_many_small_requests(self, async_http_client_factory, gunicorn_url):\n        \"\"\"Test many small requests.\"\"\"\n        async with await async_http_client_factory() as client:\n            tasks = [\n                client.get(f\"{gunicorn_url}/http/health\")\n                for _ in range(200)\n            ]\n            results = await asyncio.gather(*tasks, return_exceptions=True)\n\n            success = sum(\n                1 for r in results\n                if not isinstance(r, Exception) and r.status_code == 200\n            )\n            assert success >= 180  # Allow some failures\n\n    async def test_concurrent_large_responses(\n        self, async_http_client_factory, gunicorn_url\n    ):\n        \"\"\"Test concurrent large response handling.\"\"\"\n        async with await async_http_client_factory(timeout=60.0) as client:\n            async def large_request():\n                response = await client.get(\n                    f\"{gunicorn_url}/stream/large-stream?size=102400\"  # 100KB\n                )\n                return len(response.content) == 102400\n\n            tasks = [large_request() for _ in range(10)]\n            results = await asyncio.gather(*tasks)\n\n            assert all(results)\n\n\n# ============================================================================\n# Proxy Concurrency Tests\n# ============================================================================\n\n@pytest.mark.asyncio\nclass TestProxyConcurrency:\n    \"\"\"Test concurrent access through proxy.\"\"\"\n\n    async def test_proxy_concurrent_http(self, async_http_client_factory, nginx_url):\n        \"\"\"Test concurrent HTTP through proxy.\"\"\"\n        async with await async_http_client_factory() as client:\n            tasks = [\n                client.get(f\"{nginx_url}/http/\")\n                for _ in range(30)\n            ]\n            results = await asyncio.gather(*tasks, return_exceptions=True)\n\n            # Allow for some failures in concurrent proxy requests\n            successes = [r for r in results if not isinstance(r, Exception) and r.status_code == 200]\n            assert len(successes) >= 25  # At least 25/30 should succeed\n\n    async def test_proxy_concurrent_websocket(self, websocket_connect, nginx_url):\n        \"\"\"Test concurrent WebSocket through proxy.\"\"\"\n        ws_url = nginx_url.replace(\"http://\", \"ws://\") + \"/ws/echo\"\n\n        async def ws_echo(i):\n            async with await websocket_connect(ws_url) as ws:\n                await ws.send(f\"proxy_{i}\")\n                response = await ws.recv()\n                return response == f\"proxy_{i}\"\n\n        tasks = [ws_echo(i) for i in range(10)]\n        results = await asyncio.gather(*tasks)\n\n        assert all(results)\n\n\n# ============================================================================\n# HTTPS Concurrency Tests\n# ============================================================================\n\n@pytest.mark.ssl\n@pytest.mark.asyncio\nclass TestHTTPSConcurrency:\n    \"\"\"Test concurrent HTTPS access.\"\"\"\n\n    async def test_https_concurrent_http(\n        self, async_http_client_factory, gunicorn_ssl_url\n    ):\n        \"\"\"Test concurrent HTTPS requests.\"\"\"\n        async with await async_http_client_factory() as client:\n            tasks = [\n                client.get(f\"{gunicorn_ssl_url}/http/\")\n                for _ in range(20)\n            ]\n            results = await asyncio.gather(*tasks)\n\n            assert all(r.status_code == 200 for r in results)\n\n    async def test_https_concurrent_websocket(\n        self, websocket_connect, gunicorn_ssl_url\n    ):\n        \"\"\"Test concurrent WebSocket over HTTPS.\"\"\"\n        ws_url = gunicorn_ssl_url.replace(\"https://\", \"wss://\") + \"/ws/echo\"\n\n        async def ws_echo(i):\n            async with await websocket_connect(ws_url) as ws:\n                await ws.send(f\"secure_{i}\")\n                response = await ws.recv()\n                return response == f\"secure_{i}\"\n\n        tasks = [ws_echo(i) for i in range(10)]\n        results = await asyncio.gather(*tasks)\n\n        assert all(results)\n\n\n# ============================================================================\n# Stress Tests\n# ============================================================================\n\n@pytest.mark.asyncio\nclass TestStress:\n    \"\"\"Stress tests for edge cases.\"\"\"\n\n    async def test_rapid_connect_disconnect(\n        self, async_http_client_factory, gunicorn_url\n    ):\n        \"\"\"Test rapid connection and disconnection.\"\"\"\n        for _ in range(20):\n            async with await async_http_client_factory() as client:\n                response = await client.get(f\"{gunicorn_url}/http/\")\n                assert response.status_code == 200\n\n    async def test_rapid_websocket_connect_disconnect(\n        self, websocket_connect, gunicorn_url\n    ):\n        \"\"\"Test rapid WebSocket connect/disconnect.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/echo\"\n\n        for i in range(20):\n            async with await websocket_connect(ws_url) as ws:\n                await ws.send(f\"rapid_{i}\")\n                response = await ws.recv()\n                assert response == f\"rapid_{i}\"\n\n    async def test_mixed_success_and_error_paths(\n        self, async_http_client_factory, gunicorn_url\n    ):\n        \"\"\"Test mixed success and error responses concurrently.\"\"\"\n        async with await async_http_client_factory() as client:\n            async def success_request():\n                return await client.get(f\"{gunicorn_url}/http/\")\n\n            async def error_request():\n                return await client.get(f\"{gunicorn_url}/http/status?code=500\")\n\n            async def not_found_request():\n                return await client.get(f\"{gunicorn_url}/http/nonexistent\")\n\n            tasks = [\n                success_request(),\n                error_request(),\n                not_found_request(),\n                success_request(),\n                error_request(),\n                not_found_request(),\n            ]\n\n            results = await asyncio.gather(*tasks)\n\n            # Check expected status codes\n            expected = [200, 500, 404, 200, 500, 404]\n            for result, expected_status in zip(results, expected):\n                assert result.status_code == expected_status\n"
  },
  {
    "path": "tests/docker/asgi_compliance/test_framework_integration.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nFramework integration tests for ASGI.\n\nTests integration with popular ASGI frameworks like Starlette and FastAPI.\n\"\"\"\n\nimport json\n\nimport pytest\n\npytestmark = [\n    pytest.mark.docker,\n    pytest.mark.asgi,\n    pytest.mark.framework,\n    pytest.mark.integration,\n]\n\n\n# ============================================================================\n# Framework Availability Tests\n# ============================================================================\n\nclass TestFrameworkAvailability:\n    \"\"\"Test framework availability.\"\"\"\n\n    def test_framework_root_endpoint(self, http_client, gunicorn_url):\n        \"\"\"Test framework root returns available frameworks.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/framework/\")\n        assert response.status_code == 200\n        data = response.json()\n        assert \"apps\" in data\n        assert \"starlette\" in data[\"apps\"]\n        assert \"fastapi\" in data[\"apps\"]\n\n    def test_framework_health(self, http_client, gunicorn_url):\n        \"\"\"Test framework health endpoint.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/framework/health\")\n        assert response.status_code == 200\n\n\n# ============================================================================\n# Starlette Integration Tests\n# ============================================================================\n\nclass TestStarletteBasic:\n    \"\"\"Test basic Starlette integration.\"\"\"\n\n    def test_starlette_homepage(self, http_client, gunicorn_url):\n        \"\"\"Test Starlette homepage.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/framework/starlette/\")\n        if response.status_code == 503:\n            pytest.skip(\"Starlette not available in container\")\n        assert response.status_code == 200\n        assert \"Starlette\" in response.text\n\n    def test_starlette_json(self, http_client, gunicorn_url):\n        \"\"\"Test Starlette JSON response.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/framework/starlette/json\")\n        if response.status_code == 503:\n            pytest.skip(\"Starlette not available\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"framework\"] == \"starlette\"\n\n    def test_starlette_json_query_params(self, http_client, gunicorn_url):\n        \"\"\"Test Starlette query parameters.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/framework/starlette/json?foo=bar&baz=123\")\n        if response.status_code == 503:\n            pytest.skip(\"Starlette not available\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"query_params\"][\"foo\"] == \"bar\"\n        assert data[\"query_params\"][\"baz\"] == \"123\"\n\n    def test_starlette_echo(self, http_client, gunicorn_url):\n        \"\"\"Test Starlette echo endpoint.\"\"\"\n        body = \"Hello Starlette!\"\n        response = http_client.post(\n            f\"{gunicorn_url}/framework/starlette/echo\",\n            content=body.encode()\n        )\n        if response.status_code == 503:\n            pytest.skip(\"Starlette not available\")\n        assert response.status_code == 200\n        assert body in response.text\n\n    def test_starlette_headers(self, http_client, gunicorn_url):\n        \"\"\"Test Starlette headers endpoint.\"\"\"\n        response = http_client.get(\n            f\"{gunicorn_url}/framework/starlette/headers\",\n            headers={\"X-Custom-Header\": \"custom-value\"}\n        )\n        if response.status_code == 503:\n            pytest.skip(\"Starlette not available\")\n        assert response.status_code == 200\n        data = response.json()\n        assert \"x-custom-header\" in data\n        assert data[\"x-custom-header\"] == \"custom-value\"\n\n    def test_starlette_scope(self, http_client, gunicorn_url):\n        \"\"\"Test Starlette scope endpoint.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/framework/starlette/scope\")\n        if response.status_code == 503:\n            pytest.skip(\"Starlette not available\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"type\"] == \"http\"\n        assert \"asgi\" in data\n\n    def test_starlette_health(self, http_client, gunicorn_url):\n        \"\"\"Test Starlette health endpoint.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/framework/starlette/health\")\n        if response.status_code == 503:\n            pytest.skip(\"Starlette not available\")\n        assert response.status_code == 200\n\n\nclass TestStarletteStreaming:\n    \"\"\"Test Starlette streaming functionality.\"\"\"\n\n    def test_starlette_streaming(self, http_client, gunicorn_url):\n        \"\"\"Test Starlette streaming response.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/framework/starlette/streaming\")\n        if response.status_code == 503:\n            pytest.skip(\"Starlette not available\")\n        assert response.status_code == 200\n        assert \"Chunk\" in response.text\n\n    def test_starlette_streaming_chunks(self, streaming_client, gunicorn_url):\n        \"\"\"Test Starlette streaming returns multiple chunks.\"\"\"\n        try:\n            chunks = list(streaming_client.stream_chunks(\n                f\"{gunicorn_url}/framework/starlette/streaming\"\n            ))\n        except Exception:\n            pytest.skip(\"Starlette not available\")\n\n        full_content = b\"\".join(chunks).decode(\"utf-8\")\n        if \"Framework not available\" in full_content:\n            pytest.skip(\"Starlette not available\")\n        assert \"Chunk 1\" in full_content\n        assert \"Chunk 10\" in full_content\n\n\nclass TestStarletteWebSocket:\n    \"\"\"Test Starlette WebSocket functionality.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_starlette_websocket_echo(self, websocket_connect, gunicorn_url):\n        \"\"\"Test Starlette WebSocket echo.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/framework/starlette/ws/echo\"\n        try:\n            async with await websocket_connect(ws_url) as ws:\n                await ws.send(\"hello starlette\")\n                response = await ws.recv()\n                assert \"Starlette echo: hello starlette\" in response\n        except Exception as e:\n            if \"403\" in str(e) or \"404\" in str(e):\n                pytest.skip(\"Starlette WebSocket not available\")\n            raise\n\n\n# ============================================================================\n# FastAPI Integration Tests\n# ============================================================================\n\nclass TestFastAPIBasic:\n    \"\"\"Test basic FastAPI integration.\"\"\"\n\n    def test_fastapi_homepage(self, http_client, gunicorn_url):\n        \"\"\"Test FastAPI homepage.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/framework/fastapi/\")\n        if response.status_code == 503:\n            pytest.skip(\"FastAPI not available in container\")\n        assert response.status_code == 200\n        data = response.json()\n        assert \"FastAPI\" in data.get(\"message\", \"\")\n\n    def test_fastapi_json(self, http_client, gunicorn_url):\n        \"\"\"Test FastAPI JSON response.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/framework/fastapi/json\")\n        if response.status_code == 503:\n            pytest.skip(\"FastAPI not available\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"framework\"] == \"fastapi\"\n\n    def test_fastapi_json_query_params(self, http_client, gunicorn_url):\n        \"\"\"Test FastAPI query parameters.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/framework/fastapi/json?foo=bar&num=42\")\n        if response.status_code == 503:\n            pytest.skip(\"FastAPI not available\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"query_params\"][\"foo\"] == \"bar\"\n        assert data[\"query_params\"][\"num\"] == \"42\"\n\n    def test_fastapi_echo(self, http_client, gunicorn_url):\n        \"\"\"Test FastAPI echo endpoint.\"\"\"\n        body = \"Hello FastAPI!\"\n        response = http_client.post(\n            f\"{gunicorn_url}/framework/fastapi/echo\",\n            content=body.encode()\n        )\n        if response.status_code == 503:\n            pytest.skip(\"FastAPI not available\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"echo\"] == body\n        assert data[\"length\"] == len(body)\n\n    def test_fastapi_headers(self, http_client, gunicorn_url):\n        \"\"\"Test FastAPI headers endpoint.\"\"\"\n        response = http_client.get(\n            f\"{gunicorn_url}/framework/fastapi/headers\",\n            headers={\"X-FastAPI-Header\": \"fastapi-value\"}\n        )\n        if response.status_code == 503:\n            pytest.skip(\"FastAPI not available\")\n        assert response.status_code == 200\n        data = response.json()\n        assert \"x-fastapi-header\" in data\n        assert data[\"x-fastapi-header\"] == \"fastapi-value\"\n\n    def test_fastapi_scope(self, http_client, gunicorn_url):\n        \"\"\"Test FastAPI scope endpoint.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/framework/fastapi/scope\")\n        if response.status_code == 503:\n            pytest.skip(\"FastAPI not available\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"type\"] == \"http\"\n        assert \"asgi\" in data\n\n    def test_fastapi_health(self, http_client, gunicorn_url):\n        \"\"\"Test FastAPI health endpoint.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/framework/fastapi/health\")\n        if response.status_code == 503:\n            pytest.skip(\"FastAPI not available\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"status\"] == \"ok\"\n\n\nclass TestFastAPIPathParameters:\n    \"\"\"Test FastAPI path parameters.\"\"\"\n\n    def test_path_parameter_int(self, http_client, gunicorn_url):\n        \"\"\"Test FastAPI path parameter with integer.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/framework/fastapi/items/42\")\n        if response.status_code == 503:\n            pytest.skip(\"FastAPI not available\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"item_id\"] == 42\n\n    def test_path_parameter_with_query(self, http_client, gunicorn_url):\n        \"\"\"Test FastAPI path parameter with query string.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/framework/fastapi/items/123?q=search\")\n        if response.status_code == 503:\n            pytest.skip(\"FastAPI not available\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"item_id\"] == 123\n        assert data[\"query\"] == \"search\"\n\n    def test_create_item(self, http_client, gunicorn_url):\n        \"\"\"Test FastAPI create item endpoint.\"\"\"\n        item = {\"name\": \"Test Item\", \"price\": 99.99}\n        response = http_client.post(\n            f\"{gunicorn_url}/framework/fastapi/items/\",\n            json=item\n        )\n        if response.status_code == 503:\n            pytest.skip(\"FastAPI not available\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"created\"] == item\n\n\nclass TestFastAPIStreaming:\n    \"\"\"Test FastAPI streaming functionality.\"\"\"\n\n    def test_fastapi_streaming(self, http_client, gunicorn_url):\n        \"\"\"Test FastAPI streaming response.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/framework/fastapi/streaming\")\n        if response.status_code == 503:\n            pytest.skip(\"FastAPI not available\")\n        assert response.status_code == 200\n        assert \"Chunk\" in response.text\n\n    def test_fastapi_streaming_chunks(self, streaming_client, gunicorn_url):\n        \"\"\"Test FastAPI streaming returns multiple chunks.\"\"\"\n        try:\n            chunks = list(streaming_client.stream_chunks(\n                f\"{gunicorn_url}/framework/fastapi/streaming\"\n            ))\n        except Exception:\n            pytest.skip(\"FastAPI not available\")\n\n        full_content = b\"\".join(chunks).decode(\"utf-8\")\n        if \"Framework not available\" in full_content:\n            pytest.skip(\"FastAPI not available\")\n        assert \"Chunk 1\" in full_content\n        assert \"Chunk 10\" in full_content\n\n\nclass TestFastAPIWebSocket:\n    \"\"\"Test FastAPI WebSocket functionality.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_fastapi_websocket_echo(self, websocket_connect, gunicorn_url):\n        \"\"\"Test FastAPI WebSocket echo.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/framework/fastapi/ws/echo\"\n        try:\n            async with await websocket_connect(ws_url) as ws:\n                await ws.send(\"hello fastapi\")\n                response = await ws.recv()\n                assert \"FastAPI echo: hello fastapi\" in response\n        except Exception as e:\n            if \"403\" in str(e) or \"404\" in str(e):\n                pytest.skip(\"FastAPI WebSocket not available\")\n            raise\n\n\n# ============================================================================\n# Cross-Framework Tests\n# ============================================================================\n\nclass TestCrossFramework:\n    \"\"\"Test cross-framework functionality.\"\"\"\n\n    def test_both_frameworks_available(self, http_client, gunicorn_url):\n        \"\"\"Test both frameworks are available.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/framework/\")\n        assert response.status_code == 200\n        data = response.json()\n\n        starlette_available = data[\"apps\"][\"starlette\"][\"available\"]\n        fastapi_available = data[\"apps\"][\"fastapi\"][\"available\"]\n\n        # At least one should be available (container should have them)\n        # If neither available, skip\n        if not starlette_available and not fastapi_available:\n            pytest.skip(\"No frameworks available\")\n\n    def test_framework_independence(self, http_client, gunicorn_url):\n        \"\"\"Test frameworks work independently.\"\"\"\n        # Check framework root first\n        root_response = http_client.get(f\"{gunicorn_url}/framework/\")\n        if root_response.status_code != 200:\n            pytest.skip(\"Frameworks not available\")\n\n        data = root_response.json()\n\n        if data[\"apps\"][\"starlette\"][\"available\"]:\n            starlette_response = http_client.get(f\"{gunicorn_url}/framework/starlette/health\")\n            assert starlette_response.status_code == 200\n\n        if data[\"apps\"][\"fastapi\"][\"available\"]:\n            fastapi_response = http_client.get(f\"{gunicorn_url}/framework/fastapi/health\")\n            assert fastapi_response.status_code == 200\n\n\n# ============================================================================\n# Proxy Framework Tests\n# ============================================================================\n\nclass TestProxyFramework:\n    \"\"\"Test frameworks through nginx proxy.\"\"\"\n\n    def test_proxy_framework_root(self, http_client, nginx_url):\n        \"\"\"Test framework root through proxy.\"\"\"\n        response = http_client.get(f\"{nginx_url}/framework/\")\n        assert response.status_code == 200\n        data = response.json()\n        assert \"apps\" in data\n\n    def test_proxy_starlette(self, http_client, nginx_url):\n        \"\"\"Test Starlette through proxy.\"\"\"\n        response = http_client.get(f\"{nginx_url}/framework/starlette/json\")\n        if response.status_code == 503:\n            pytest.skip(\"Starlette not available\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"framework\"] == \"starlette\"\n\n    def test_proxy_fastapi(self, http_client, nginx_url):\n        \"\"\"Test FastAPI through proxy.\"\"\"\n        response = http_client.get(f\"{nginx_url}/framework/fastapi/json\")\n        if response.status_code == 503:\n            pytest.skip(\"FastAPI not available\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"framework\"] == \"fastapi\"\n\n\n# ============================================================================\n# HTTPS Framework Tests\n# ============================================================================\n\n@pytest.mark.ssl\nclass TestHTTPSFramework:\n    \"\"\"Test frameworks over HTTPS.\"\"\"\n\n    def test_https_starlette(self, http_client, gunicorn_ssl_url):\n        \"\"\"Test Starlette over HTTPS.\"\"\"\n        response = http_client.get(f\"{gunicorn_ssl_url}/framework/starlette/json\")\n        if response.status_code == 503:\n            pytest.skip(\"Starlette not available\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"framework\"] == \"starlette\"\n\n    def test_https_fastapi(self, http_client, gunicorn_ssl_url):\n        \"\"\"Test FastAPI over HTTPS.\"\"\"\n        response = http_client.get(f\"{gunicorn_ssl_url}/framework/fastapi/json\")\n        if response.status_code == 503:\n            pytest.skip(\"FastAPI not available\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"framework\"] == \"fastapi\"\n\n    def test_https_proxy_starlette(self, http_client, nginx_ssl_url):\n        \"\"\"Test Starlette through HTTPS proxy.\"\"\"\n        response = http_client.get(f\"{nginx_ssl_url}/framework/starlette/health\")\n        if response.status_code == 503:\n            pytest.skip(\"Starlette not available\")\n        assert response.status_code == 200\n\n    def test_https_proxy_fastapi(self, http_client, nginx_ssl_url):\n        \"\"\"Test FastAPI through HTTPS proxy.\"\"\"\n        import time\n        response = None\n        # Retry up to 3 times for intermittent proxy connectivity issues\n        for attempt in range(3):\n            response = http_client.get(f\"{nginx_ssl_url}/framework/fastapi/health\")\n            if response.status_code == 503:\n                pytest.skip(\"FastAPI not available\")\n            if response.status_code == 200:\n                break\n            time.sleep(0.5)\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"status\"] == \"ok\"\n\n\n# ============================================================================\n# Async Framework Tests\n# ============================================================================\n\n@pytest.mark.asyncio\nclass TestAsyncFramework:\n    \"\"\"Test frameworks with async client.\"\"\"\n\n    async def test_async_starlette(self, async_http_client_factory, gunicorn_url):\n        \"\"\"Test Starlette with async client.\"\"\"\n        async with await async_http_client_factory() as client:\n            response = await client.get(f\"{gunicorn_url}/framework/starlette/json\")\n            if response.status_code == 503:\n                pytest.skip(\"Starlette not available\")\n            assert response.status_code == 200\n            data = response.json()\n            assert data[\"framework\"] == \"starlette\"\n\n    async def test_async_fastapi(self, async_http_client_factory, gunicorn_url):\n        \"\"\"Test FastAPI with async client.\"\"\"\n        async with await async_http_client_factory() as client:\n            response = await client.get(f\"{gunicorn_url}/framework/fastapi/json\")\n            if response.status_code == 503:\n                pytest.skip(\"FastAPI not available\")\n            assert response.status_code == 200\n            data = response.json()\n            assert data[\"framework\"] == \"fastapi\"\n\n    async def test_concurrent_framework_requests(self, async_http_client_factory, gunicorn_url):\n        \"\"\"Test concurrent requests to both frameworks.\"\"\"\n        import asyncio\n\n        async with await async_http_client_factory() as client:\n            async def get_starlette():\n                response = await client.get(f\"{gunicorn_url}/framework/starlette/json\")\n                return response.status_code, \"starlette\"\n\n            async def get_fastapi():\n                response = await client.get(f\"{gunicorn_url}/framework/fastapi/json\")\n                return response.status_code, \"fastapi\"\n\n            results = await asyncio.gather(\n                get_starlette(),\n                get_fastapi(),\n                get_starlette(),\n                get_fastapi(),\n            )\n\n            # All should either succeed (200) or framework unavailable (503)\n            for status, name in results:\n                assert status in [200, 503], f\"{name} returned {status}\"\n"
  },
  {
    "path": "tests/docker/asgi_compliance/test_http2_asgi.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nHTTP/2 ASGI integration tests.\n\nTests HTTP/2 specific functionality with ASGI applications.\n\"\"\"\n\nimport json\n\nimport pytest\n\npytestmark = [\n    pytest.mark.docker,\n    pytest.mark.asgi,\n    pytest.mark.http2,\n    pytest.mark.integration,\n]\n\n\n# ============================================================================\n# HTTP/2 Basic Tests\n# ============================================================================\n\nclass TestHTTP2Basic:\n    \"\"\"Test basic HTTP/2 functionality with ASGI.\"\"\"\n\n    def test_http2_request(self, http2_client, nginx_ssl_url):\n        \"\"\"Test HTTP/2 request through nginx.\"\"\"\n        response = http2_client.get(f\"{nginx_ssl_url}/http/\")\n        assert response.status_code == 200\n        # HTTP/2 is negotiated via ALPN on TLS\n        assert response.http_version in [\"HTTP/2\", \"HTTP/1.1\"]\n\n    def test_http2_scope(self, http2_client, nginx_ssl_url):\n        \"\"\"Test ASGI scope with HTTP/2.\"\"\"\n        response = http2_client.get(f\"{nginx_ssl_url}/http/scope\")\n        assert response.status_code == 200\n        data = response.json()\n        # HTTP version in scope should reflect what the app sees\n        # (may be 1.1 if nginx proxies as HTTP/1.1 to backend)\n        assert data[\"http_version\"] in [\"1.1\", \"2\", \"1.0\"]\n\n    def test_http2_headers(self, http2_client, nginx_ssl_url):\n        \"\"\"Test headers work correctly over HTTP/2.\"\"\"\n        response = http2_client.get(\n            f\"{nginx_ssl_url}/http/headers\",\n            headers={\n                \"X-Custom-Header\": \"http2-value\",\n                \"X-Another-Header\": \"another-value\",\n            }\n        )\n        assert response.status_code == 200\n        data = response.json()\n        assert \"x-custom-header\" in data\n        assert data[\"x-custom-header\"] == \"http2-value\"\n\n\n# ============================================================================\n# HTTP/2 Multiplexing Tests\n# ============================================================================\n\n@pytest.mark.asyncio\nclass TestHTTP2Multiplexing:\n    \"\"\"Test HTTP/2 multiplexing features.\"\"\"\n\n    async def test_concurrent_requests_single_connection(\n        self, async_http_client_factory, nginx_ssl_url\n    ):\n        \"\"\"Test concurrent requests on single HTTP/2 connection.\"\"\"\n        import asyncio\n\n        async with await async_http_client_factory(http2=True) as client:\n            async def make_request(i):\n                response = await client.get(f\"{nginx_ssl_url}/http/?req={i}\")\n                return response.status_code == 200, i\n\n            # HTTP/2 allows multiple concurrent streams\n            tasks = [make_request(i) for i in range(20)]\n            results = await asyncio.gather(*tasks)\n\n            assert all(success for success, _ in results)\n\n    async def test_interleaved_requests(\n        self, async_http_client_factory, nginx_ssl_url\n    ):\n        \"\"\"Test interleaved request/response on HTTP/2.\"\"\"\n        import asyncio\n\n        async with await async_http_client_factory(http2=True) as client:\n            async def fast_request():\n                return await client.get(f\"{nginx_ssl_url}/http/health\")\n\n            async def slow_request():\n                return await client.get(f\"{nginx_ssl_url}/http/delay?ms=100\")\n\n            # Mix of fast and slow requests\n            tasks = [\n                slow_request(),\n                fast_request(),\n                slow_request(),\n                fast_request(),\n                fast_request(),\n            ]\n\n            results = await asyncio.gather(*tasks)\n            assert all(r.status_code == 200 for r in results)\n\n\n# ============================================================================\n# HTTP/2 Streaming Tests\n# ============================================================================\n\nclass TestHTTP2Streaming:\n    \"\"\"Test HTTP/2 streaming with ASGI.\"\"\"\n\n    def test_http2_streaming_response(self, http2_client, nginx_ssl_url):\n        \"\"\"Test streaming response over HTTP/2.\"\"\"\n        response = http2_client.get(f\"{nginx_ssl_url}/stream/streaming?chunks=5\")\n        assert response.status_code == 200\n        assert \"Chunk\" in response.text\n\n    def test_http2_sse(self, http2_client, nginx_ssl_url):\n        \"\"\"Test Server-Sent Events over HTTP/2.\"\"\"\n        response = http2_client.get(f\"{nginx_ssl_url}/stream/sse?events=3&delay=0.1\")\n        assert response.status_code == 200\n        assert \"text/event-stream\" in response.headers.get(\"content-type\", \"\")\n\n    def test_http2_large_response(self, http2_client, nginx_ssl_url):\n        \"\"\"Test large response over HTTP/2.\"\"\"\n        response = http2_client.get(f\"{nginx_ssl_url}/stream/large-stream?size=102400\")\n        assert response.status_code == 200\n        assert len(response.content) == 102400\n\n\n# ============================================================================\n# HTTP/2 POST/Body Tests\n# ============================================================================\n\nclass TestHTTP2RequestBody:\n    \"\"\"Test HTTP/2 request body handling.\"\"\"\n\n    def test_http2_post_json(self, http2_client, nginx_ssl_url):\n        \"\"\"Test POST with JSON body over HTTP/2.\"\"\"\n        data = {\"message\": \"http2 post\", \"number\": 42}\n        response = http2_client.post(\n            f\"{nginx_ssl_url}/http/post-json\",\n            json=data\n        )\n        assert response.status_code == 200\n        result = response.json()\n        assert result[\"received\"][\"message\"] == \"http2 post\"\n\n    def test_http2_post_echo(self, http2_client, nginx_ssl_url):\n        \"\"\"Test echo endpoint over HTTP/2.\"\"\"\n        body = b\"HTTP/2 echo test body\"\n        response = http2_client.post(\n            f\"{nginx_ssl_url}/http/echo\",\n            content=body\n        )\n        assert response.status_code == 200\n        assert response.content == body\n\n    def test_http2_large_request_body(self, http2_client, nginx_ssl_url):\n        \"\"\"Test large request body over HTTP/2.\"\"\"\n        body = b\"x\" * 100000  # 100KB\n        response = http2_client.post(\n            f\"{nginx_ssl_url}/http/echo\",\n            content=body\n        )\n        assert response.status_code == 200\n        assert len(response.content) == 100000\n\n\n# ============================================================================\n# HTTP/2 ASGI Scope Tests\n# ============================================================================\n\nclass TestHTTP2ASGIScope:\n    \"\"\"Test ASGI scope properties with HTTP/2.\"\"\"\n\n    def test_scope_type_http(self, http2_client, nginx_ssl_url):\n        \"\"\"Test scope type is HTTP.\"\"\"\n        response = http2_client.get(f\"{nginx_ssl_url}/http/scope\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"type\"] == \"http\"\n\n    def test_scope_asgi_version(self, http2_client, nginx_ssl_url):\n        \"\"\"Test ASGI version in scope.\"\"\"\n        response = http2_client.get(f\"{nginx_ssl_url}/http/scope\")\n        assert response.status_code == 200\n        data = response.json()\n        assert \"asgi\" in data\n        assert \"version\" in data[\"asgi\"]\n\n    def test_scope_scheme_https(self, http2_client, nginx_ssl_url):\n        \"\"\"Test scheme is HTTPS in scope.\"\"\"\n        response = http2_client.get(f\"{nginx_ssl_url}/http/scope\")\n        assert response.status_code == 200\n        data = response.json()\n        # Scope scheme reflects what app sees (may be http if proxy strips TLS)\n        assert data[\"scheme\"] in [\"http\", \"https\"]\n\n    def test_scope_method_preserved(self, http2_client, nginx_ssl_url):\n        \"\"\"Test HTTP method is preserved in scope.\"\"\"\n        response = http2_client.get(f\"{nginx_ssl_url}/http/scope\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"method\"] == \"GET\"\n\n    def test_scope_path_preserved(self, http2_client, nginx_ssl_url):\n        \"\"\"Test path is preserved in scope.\"\"\"\n        response = http2_client.get(f\"{nginx_ssl_url}/http/scope\")\n        assert response.status_code == 200\n        data = response.json()\n        # Path is stripped by main_app router (/http prefix removed)\n        assert data[\"path\"] == \"/scope\"\n\n    def test_scope_query_string(self, http2_client, nginx_ssl_url):\n        \"\"\"Test query string in scope.\"\"\"\n        response = http2_client.get(f\"{nginx_ssl_url}/http/scope?foo=bar&baz=qux\")\n        assert response.status_code == 200\n        data = response.json()\n        assert \"foo=bar\" in data[\"query_string\"]\n\n\n# ============================================================================\n# HTTP/2 Framework Tests\n# ============================================================================\n\nclass TestHTTP2Framework:\n    \"\"\"Test frameworks over HTTP/2.\"\"\"\n\n    def test_http2_starlette(self, http2_client, nginx_ssl_url):\n        \"\"\"Test Starlette over HTTP/2.\"\"\"\n        response = http2_client.get(f\"{nginx_ssl_url}/framework/starlette/json\")\n        if response.status_code == 503:\n            pytest.skip(\"Starlette not available\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"framework\"] == \"starlette\"\n\n    def test_http2_fastapi(self, http2_client, nginx_ssl_url):\n        \"\"\"Test FastAPI over HTTP/2.\"\"\"\n        response = http2_client.get(f\"{nginx_ssl_url}/framework/fastapi/json\")\n        if response.status_code == 503:\n            pytest.skip(\"FastAPI not available\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"framework\"] == \"fastapi\"\n\n\n# ============================================================================\n# HTTP/2 Error Handling Tests\n# ============================================================================\n\nclass TestHTTP2Errors:\n    \"\"\"Test HTTP/2 error handling.\"\"\"\n\n    def test_http2_404(self, http2_client, nginx_ssl_url):\n        \"\"\"Test 404 over HTTP/2.\"\"\"\n        response = http2_client.get(f\"{nginx_ssl_url}/http/nonexistent\")\n        assert response.status_code == 404\n\n    def test_http2_500(self, http2_client, nginx_ssl_url):\n        \"\"\"Test 500 over HTTP/2.\"\"\"\n        response = http2_client.get(f\"{nginx_ssl_url}/http/status?code=500\")\n        assert response.status_code == 500\n\n    def test_http2_various_status_codes(self, http2_client, nginx_ssl_url):\n        \"\"\"Test various status codes over HTTP/2.\"\"\"\n        for code in [200, 201, 204, 301, 400, 403, 404, 500, 503]:\n            response = http2_client.get(\n                f\"{nginx_ssl_url}/http/status?code={code}\",\n                follow_redirects=False\n            )\n            assert response.status_code == code\n\n\n# ============================================================================\n# HTTP/2 Concurrent Async Tests\n# ============================================================================\n\n@pytest.mark.asyncio\nclass TestHTTP2Async:\n    \"\"\"Test async HTTP/2 operations.\"\"\"\n\n    async def test_async_http2_streaming(\n        self, async_http_client_factory, nginx_ssl_url\n    ):\n        \"\"\"Test async streaming over HTTP/2.\"\"\"\n        async with await async_http_client_factory(http2=True) as client:\n            chunks = []\n            async with client.stream(\n                \"GET\",\n                f\"{nginx_ssl_url}/stream/streaming?chunks=5\"\n            ) as response:\n                async for chunk in response.aiter_bytes():\n                    chunks.append(chunk)\n\n            full_content = b\"\".join(chunks).decode(\"utf-8\")\n            assert \"Chunk\" in full_content\n\n    async def test_async_http2_concurrent_streams(\n        self, async_http_client_factory, nginx_ssl_url\n    ):\n        \"\"\"Test concurrent HTTP/2 streams.\"\"\"\n        import asyncio\n\n        async with await async_http_client_factory(http2=True) as client:\n            async def stream_request(i):\n                response = await client.get(\n                    f\"{nginx_ssl_url}/stream/streaming?chunks=3\"\n                )\n                return i, \"Chunk\" in response.text\n\n            tasks = [stream_request(i) for i in range(10)]\n            results = await asyncio.gather(*tasks)\n\n            assert all(success for _, success in results)\n\n    async def test_async_http2_mixed_requests(\n        self, async_http_client_factory, nginx_ssl_url\n    ):\n        \"\"\"Test mixed request types over HTTP/2.\"\"\"\n        import asyncio\n\n        async with await async_http_client_factory(http2=True) as client:\n            async def get_request():\n                return await client.get(f\"{nginx_ssl_url}/http/\")\n\n            async def post_request():\n                return await client.post(\n                    f\"{nginx_ssl_url}/http/echo\",\n                    content=b\"test\"\n                )\n\n            async def stream_request():\n                response = await client.get(\n                    f\"{nginx_ssl_url}/stream/streaming?chunks=2\"\n                )\n                return response\n\n            tasks = [\n                get_request(),\n                post_request(),\n                stream_request(),\n                get_request(),\n                post_request(),\n            ]\n\n            results = await asyncio.gather(*tasks)\n            assert all(r.status_code == 200 for r in results)\n\n\n# ============================================================================\n# HTTP/2 Lifespan Tests\n# ============================================================================\n\nclass TestHTTP2Lifespan:\n    \"\"\"Test lifespan app over HTTP/2.\"\"\"\n\n    def test_http2_lifespan_state(self, http2_client, nginx_ssl_url):\n        \"\"\"Test lifespan state over HTTP/2.\"\"\"\n        response = http2_client.get(f\"{nginx_ssl_url}/lifespan/state\")\n        assert response.status_code == 200\n        data = response.json()\n        # main_app handles lifespan, so check scope_state not module_state\n        assert data[\"scope_state\"][\"main_app_started\"] is True\n\n    def test_http2_lifespan_counter(self, http2_client, nginx_ssl_url):\n        \"\"\"Test lifespan counter over HTTP/2.\"\"\"\n        response = http2_client.get(f\"{nginx_ssl_url}/lifespan/counter\")\n        assert response.status_code == 200\n        data = response.json()\n        assert \"counter\" in data\n\n\n# ============================================================================\n# HTTP/2 Direct (No Proxy) Tests\n# ============================================================================\n\n@pytest.mark.ssl\nclass TestHTTP2Direct:\n    \"\"\"Test HTTP/2 directly to gunicorn (if supported).\"\"\"\n\n    def test_direct_https_request(self, http_client, gunicorn_ssl_url):\n        \"\"\"Test direct HTTPS request to gunicorn.\"\"\"\n        response = http_client.get(f\"{gunicorn_ssl_url}/http/\")\n        assert response.status_code == 200\n\n    def test_direct_https_scope(self, http_client, gunicorn_ssl_url):\n        \"\"\"Test scope from direct HTTPS connection.\"\"\"\n        response = http_client.get(f\"{gunicorn_ssl_url}/http/scope\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"type\"] == \"http\"\n        # Direct connection should show https scheme\n        assert data[\"scheme\"] == \"https\"\n\n    def test_direct_https_streaming(self, http_client, gunicorn_ssl_url):\n        \"\"\"Test streaming from direct HTTPS connection.\"\"\"\n        response = http_client.get(f\"{gunicorn_ssl_url}/stream/streaming?chunks=3\")\n        assert response.status_code == 200\n        assert \"Chunk\" in response.text\n"
  },
  {
    "path": "tests/docker/asgi_compliance/test_http_compliance.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nHTTP compliance integration tests for ASGI.\n\nTests HTTP request/response handling, headers, methods, status codes,\nand ASGI scope correctness through actual HTTP requests.\n\"\"\"\n\nimport json\n\nimport pytest\n\npytestmark = [\n    pytest.mark.docker,\n    pytest.mark.asgi,\n    pytest.mark.integration,\n]\n\n\n# ============================================================================\n# Basic HTTP Request/Response Tests\n# ============================================================================\n\nclass TestBasicHTTPRequests:\n    \"\"\"Test basic HTTP request/response functionality.\"\"\"\n\n    def test_root_endpoint(self, http_client, gunicorn_url):\n        \"\"\"Test root endpoint returns expected response.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/\")\n        assert response.status_code == 200\n        assert \"ASGI Compliance Testbed\" in response.text\n\n    def test_health_endpoint(self, http_client, gunicorn_url):\n        \"\"\"Test health check endpoint.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/health\")\n        assert response.status_code == 200\n        assert response.text == \"OK\"\n\n    def test_http_app_root(self, http_client, gunicorn_url):\n        \"\"\"Test HTTP app root endpoint.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/http/\")\n        assert response.status_code == 200\n        assert response.text == \"Hello, ASGI!\"\n\n    def test_not_found(self, http_client, gunicorn_url):\n        \"\"\"Test 404 response for unknown paths.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/http/nonexistent\")\n        assert response.status_code == 404\n\n\nclass TestHTTPMethods:\n    \"\"\"Test various HTTP methods.\"\"\"\n\n    def test_get_method(self, http_client, gunicorn_url):\n        \"\"\"Test GET method.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/http/method\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"method\"] == \"GET\"\n\n    def test_post_method(self, http_client, gunicorn_url):\n        \"\"\"Test POST method.\"\"\"\n        response = http_client.post(f\"{gunicorn_url}/http/method\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"method\"] == \"POST\"\n\n    def test_put_method(self, http_client, gunicorn_url):\n        \"\"\"Test PUT method.\"\"\"\n        response = http_client.put(f\"{gunicorn_url}/http/method\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"method\"] == \"PUT\"\n\n    def test_delete_method(self, http_client, gunicorn_url):\n        \"\"\"Test DELETE method.\"\"\"\n        response = http_client.delete(f\"{gunicorn_url}/http/method\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"method\"] == \"DELETE\"\n\n    def test_patch_method(self, http_client, gunicorn_url):\n        \"\"\"Test PATCH method.\"\"\"\n        response = http_client.patch(f\"{gunicorn_url}/http/method\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"method\"] == \"PATCH\"\n\n    def test_head_method(self, http_client, gunicorn_url):\n        \"\"\"Test HEAD method returns no body.\"\"\"\n        response = http_client.head(f\"{gunicorn_url}/http/\")\n        assert response.status_code == 200\n        assert response.content == b\"\"\n\n    def test_options_method(self, http_client, gunicorn_url):\n        \"\"\"Test OPTIONS method.\"\"\"\n        response = http_client.options(f\"{gunicorn_url}/http/method\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"method\"] == \"OPTIONS\"\n\n\nclass TestHTTPStatusCodes:\n    \"\"\"Test HTTP status code responses.\"\"\"\n\n    @pytest.mark.parametrize(\"status_code\", [\n        200, 201, 202, 204, 301, 302, 304, 400, 401, 403, 404, 405, 500, 502, 503\n    ])\n    def test_status_codes(self, http_client, gunicorn_url, status_code):\n        \"\"\"Test various HTTP status codes.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/http/status?code={status_code}\")\n        assert response.status_code == status_code\n\n    def test_invalid_status_code(self, http_client, gunicorn_url):\n        \"\"\"Test invalid status code returns 400.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/http/status?code=999\")\n        assert response.status_code == 400\n\n\n# ============================================================================\n# Request/Response Body Tests\n# ============================================================================\n\nclass TestRequestBody:\n    \"\"\"Test request body handling.\"\"\"\n\n    def test_echo_small_body(self, http_client, gunicorn_url):\n        \"\"\"Test echoing small request body.\"\"\"\n        body = b\"Hello, World!\"\n        response = http_client.post(f\"{gunicorn_url}/http/echo\", content=body)\n        assert response.status_code == 200\n        assert response.content == body\n\n    def test_echo_large_body(self, http_client, gunicorn_url):\n        \"\"\"Test echoing large request body (1MB).\"\"\"\n        body = b\"x\" * (1024 * 1024)\n        response = http_client.post(f\"{gunicorn_url}/http/echo\", content=body)\n        assert response.status_code == 200\n        assert len(response.content) == len(body)\n        assert response.content == body\n\n    def test_echo_empty_body(self, http_client, gunicorn_url):\n        \"\"\"Test echoing empty request body.\"\"\"\n        response = http_client.post(f\"{gunicorn_url}/http/echo\", content=b\"\")\n        assert response.status_code == 200\n        assert response.content == b\"\"\n\n    def test_post_json(self, http_client, gunicorn_url):\n        \"\"\"Test posting and receiving JSON.\"\"\"\n        data = {\"name\": \"test\", \"value\": 123, \"nested\": {\"key\": \"value\"}}\n        response = http_client.post(\n            f\"{gunicorn_url}/http/post-json\",\n            json=data\n        )\n        assert response.status_code == 200\n        result = response.json()\n        assert result[\"received\"] == data\n        assert result[\"type\"] == \"dict\"\n\n    def test_post_json_array(self, http_client, gunicorn_url):\n        \"\"\"Test posting JSON array.\"\"\"\n        data = [1, 2, 3, \"four\", {\"five\": 5}]\n        response = http_client.post(\n            f\"{gunicorn_url}/http/post-json\",\n            json=data\n        )\n        assert response.status_code == 200\n        result = response.json()\n        assert result[\"received\"] == data\n        assert result[\"type\"] == \"list\"\n\n\nclass TestResponseBody:\n    \"\"\"Test response body handling.\"\"\"\n\n    def test_large_response(self, http_client, gunicorn_url):\n        \"\"\"Test receiving large response (1MB).\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/http/large?size=1048576\")\n        assert response.status_code == 200\n        assert len(response.content) == 1048576\n\n    def test_large_response_custom_size(self, http_client, gunicorn_url):\n        \"\"\"Test receiving custom size response.\"\"\"\n        size = 500000\n        response = http_client.get(f\"{gunicorn_url}/http/large?size={size}\")\n        assert response.status_code == 200\n        assert len(response.content) == size\n\n\n# ============================================================================\n# Header Tests\n# ============================================================================\n\nclass TestRequestHeaders:\n    \"\"\"Test request header handling.\"\"\"\n\n    def test_headers_received(self, http_client, gunicorn_url):\n        \"\"\"Test that request headers are received correctly.\"\"\"\n        response = http_client.get(\n            f\"{gunicorn_url}/http/headers\",\n            headers={\n                \"X-Custom-Header\": \"custom-value\",\n                \"X-Another-Header\": \"another-value\",\n            }\n        )\n        assert response.status_code == 200\n        headers = response.json()\n        assert headers.get(\"x-custom-header\") == \"custom-value\"\n        assert headers.get(\"x-another-header\") == \"another-value\"\n\n    def test_host_header(self, http_client, gunicorn_url):\n        \"\"\"Test Host header is received.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/http/headers\")\n        assert response.status_code == 200\n        headers = response.json()\n        assert \"host\" in headers\n\n    def test_user_agent_header(self, http_client, gunicorn_url):\n        \"\"\"Test User-Agent header is received.\"\"\"\n        response = http_client.get(\n            f\"{gunicorn_url}/http/headers\",\n            headers={\"User-Agent\": \"TestClient/1.0\"}\n        )\n        assert response.status_code == 200\n        headers = response.json()\n        assert headers.get(\"user-agent\") == \"TestClient/1.0\"\n\n    def test_content_type_header(self, http_client, gunicorn_url):\n        \"\"\"Test Content-Type header on POST.\"\"\"\n        response = http_client.post(\n            f\"{gunicorn_url}/http/headers\",\n            content=b\"test\",\n            headers={\"Content-Type\": \"application/octet-stream\"}\n        )\n        assert response.status_code == 200\n        headers = response.json()\n        assert headers.get(\"content-type\") == \"application/octet-stream\"\n\n\nclass TestResponseHeaders:\n    \"\"\"Test response header handling.\"\"\"\n\n    def test_content_type_response(self, http_client, gunicorn_url):\n        \"\"\"Test Content-Type in response.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/http/headers\")\n        assert \"application/json\" in response.headers.get(\"content-type\", \"\")\n\n    def test_content_length_response(self, http_client, gunicorn_url):\n        \"\"\"Test Content-Length in response.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/http/\")\n        assert \"content-length\" in response.headers\n\n\n# ============================================================================\n# ASGI Scope Tests\n# ============================================================================\n\nclass TestASGIScope:\n    \"\"\"Test ASGI scope correctness.\"\"\"\n\n    def test_scope_type(self, http_client, gunicorn_url):\n        \"\"\"Test scope type is 'http'.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/http/scope\")\n        assert response.status_code == 200\n        scope = response.json()\n        assert scope[\"type\"] == \"http\"\n\n    def test_scope_asgi_version(self, http_client, gunicorn_url):\n        \"\"\"Test ASGI version in scope.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/http/scope\")\n        scope = response.json()\n        assert \"asgi\" in scope\n        assert scope[\"asgi\"][\"version\"] == \"3.0\"\n\n    def test_scope_http_version(self, http_client, gunicorn_url):\n        \"\"\"Test HTTP version in scope.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/http/scope\")\n        scope = response.json()\n        assert scope[\"http_version\"] in (\"1.0\", \"1.1\", \"2\")\n\n    def test_scope_method(self, http_client, gunicorn_url):\n        \"\"\"Test method in scope.\"\"\"\n        response = http_client.post(f\"{gunicorn_url}/http/scope\")\n        scope = response.json()\n        assert scope[\"method\"] == \"POST\"\n\n    def test_scope_scheme(self, http_client, gunicorn_url):\n        \"\"\"Test scheme in scope.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/http/scope\")\n        scope = response.json()\n        assert scope[\"scheme\"] == \"http\"\n\n    def test_scope_path(self, http_client, gunicorn_url):\n        \"\"\"Test path in scope.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/http/scope\")\n        scope = response.json()\n        assert scope[\"path\"] == \"/scope\"\n\n    def test_scope_query_string(self, http_client, gunicorn_url):\n        \"\"\"Test query string in scope.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/http/scope?foo=bar&baz=qux\")\n        scope = response.json()\n        assert scope[\"query_string\"] == \"foo=bar&baz=qux\"\n\n    def test_scope_headers_are_list(self, http_client, gunicorn_url):\n        \"\"\"Test headers in scope are list of 2-tuples.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/http/scope\")\n        scope = response.json()\n        assert isinstance(scope[\"headers\"], list)\n        for header in scope[\"headers\"]:\n            assert isinstance(header, list)\n            assert len(header) == 2\n\n    def test_scope_server(self, http_client, gunicorn_url):\n        \"\"\"Test server in scope.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/http/scope\")\n        scope = response.json()\n        assert scope[\"server\"] is not None\n        assert isinstance(scope[\"server\"], list)\n        assert len(scope[\"server\"]) == 2\n\n    def test_scope_client(self, http_client, gunicorn_url):\n        \"\"\"Test client in scope.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/http/scope\")\n        scope = response.json()\n        assert scope[\"client\"] is not None\n        assert isinstance(scope[\"client\"], list)\n        assert len(scope[\"client\"]) == 2\n\n\n# ============================================================================\n# Query String Tests\n# ============================================================================\n\nclass TestQueryStrings:\n    \"\"\"Test query string handling.\"\"\"\n\n    def test_simple_query(self, http_client, gunicorn_url):\n        \"\"\"Test simple query parameter.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/http/query?name=test\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"params\"][\"name\"] == \"test\"\n\n    def test_multiple_params(self, http_client, gunicorn_url):\n        \"\"\"Test multiple query parameters.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/http/query?a=1&b=2&c=3\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"params\"][\"a\"] == \"1\"\n        assert data[\"params\"][\"b\"] == \"2\"\n        assert data[\"params\"][\"c\"] == \"3\"\n\n    def test_empty_query(self, http_client, gunicorn_url):\n        \"\"\"Test empty query string.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/http/query\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"raw\"] == \"\"\n        assert data[\"params\"] == {}\n\n    def test_url_encoded_query(self, http_client, gunicorn_url):\n        \"\"\"Test URL-encoded query parameters.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/http/query?name=hello%20world\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"raw\"] == \"name=hello%20world\"\n\n\n# ============================================================================\n# Cookie Tests\n# ============================================================================\n\nclass TestCookies:\n    \"\"\"Test cookie handling.\"\"\"\n\n    def test_set_cookie(self, http_client, gunicorn_url):\n        \"\"\"Test setting cookies.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/http/cookies?set=session=abc123\")\n        assert response.status_code == 200\n        assert \"set-cookie\" in response.headers\n\n    def test_receive_cookie(self, http_client, gunicorn_url):\n        \"\"\"Test receiving cookies.\"\"\"\n        response = http_client.get(\n            f\"{gunicorn_url}/http/cookies\",\n            cookies={\"session\": \"test123\"}\n        )\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"request_cookies\"].get(\"session\") == \"test123\"\n\n\n# ============================================================================\n# Redirect Tests\n# ============================================================================\n\nclass TestRedirects:\n    \"\"\"Test redirect handling.\"\"\"\n\n    def test_redirect_302(self, http_client, gunicorn_url):\n        \"\"\"Test 302 redirect.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/http/redirect?to=/http/&status=302\")\n        assert response.status_code == 302\n        assert response.headers.get(\"location\") == \"/http/\"\n\n    def test_redirect_301(self, http_client, gunicorn_url):\n        \"\"\"Test 301 redirect.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/http/redirect?to=/http/&status=301\")\n        assert response.status_code == 301\n\n    def test_redirect_307(self, http_client, gunicorn_url):\n        \"\"\"Test 307 redirect.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/http/redirect?to=/http/&status=307\")\n        assert response.status_code == 307\n\n\n# ============================================================================\n# Connection Tests\n# ============================================================================\n\nclass TestConnections:\n    \"\"\"Test connection handling.\"\"\"\n\n    def test_multiple_requests_same_connection(self, http_client, gunicorn_url):\n        \"\"\"Test multiple requests on same connection (keep-alive).\"\"\"\n        for i in range(5):\n            response = http_client.get(f\"{gunicorn_url}/http/\")\n            assert response.status_code == 200\n\n    def test_concurrent_requests(self, http_client, gunicorn_url):\n        \"\"\"Test concurrent requests.\"\"\"\n        import concurrent.futures\n\n        def make_request(i):\n            httpx = pytest.importorskip(\"httpx\")\n            with httpx.Client(verify=False, timeout=30.0) as client:\n                response = client.get(f\"{gunicorn_url}/http/method\")\n                return response.status_code\n\n        with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:\n            futures = [executor.submit(make_request, i) for i in range(20)]\n            results = [f.result() for f in concurrent.futures.as_completed(futures)]\n\n        assert all(status == 200 for status in results)\n\n\n# ============================================================================\n# Proxy Tests (via Nginx)\n# ============================================================================\n\nclass TestProxyRequests:\n    \"\"\"Test requests through nginx proxy.\"\"\"\n\n    def test_proxy_basic_request(self, http_client, nginx_url):\n        \"\"\"Test basic request through proxy.\"\"\"\n        response = http_client.get(f\"{nginx_url}/http/\")\n        assert response.status_code == 200\n        assert response.text == \"Hello, ASGI!\"\n\n    def test_proxy_headers_forwarded(self, http_client, nginx_url):\n        \"\"\"Test that proxy headers are forwarded.\"\"\"\n        response = http_client.get(f\"{nginx_url}/http/headers\")\n        assert response.status_code == 200\n        headers = response.json()\n        # Nginx should add X-Forwarded-For\n        assert \"x-forwarded-for\" in headers or \"x-real-ip\" in headers\n\n    def test_proxy_large_request(self, http_client, nginx_url):\n        \"\"\"Test large request through proxy.\"\"\"\n        body = b\"x\" * (100 * 1024)  # 100KB\n        response = http_client.post(f\"{nginx_url}/http/echo\", content=body)\n        assert response.status_code == 200\n        assert len(response.content) == len(body)\n\n    def test_proxy_large_response(self, http_client, nginx_url):\n        \"\"\"Test large response through proxy.\"\"\"\n        response = http_client.get(f\"{nginx_url}/http/large?size=1048576\")\n        assert response.status_code == 200\n        assert len(response.content) == 1048576\n\n\n# ============================================================================\n# HTTPS Tests\n# ============================================================================\n\n@pytest.mark.ssl\nclass TestHTTPS:\n    \"\"\"Test HTTPS connections.\"\"\"\n\n    def test_https_basic_request(self, http_client, gunicorn_ssl_url):\n        \"\"\"Test basic HTTPS request.\"\"\"\n        response = http_client.get(f\"{gunicorn_ssl_url}/http/\")\n        assert response.status_code == 200\n\n    def test_https_scope_scheme(self, http_client, gunicorn_ssl_url):\n        \"\"\"Test scope scheme is https.\"\"\"\n        response = http_client.get(f\"{gunicorn_ssl_url}/http/scope\")\n        assert response.status_code == 200\n        scope = response.json()\n        assert scope[\"scheme\"] == \"https\"\n\n    def test_https_via_proxy(self, http_client, nginx_ssl_url):\n        \"\"\"Test HTTPS through nginx proxy.\"\"\"\n        response = http_client.get(f\"{nginx_ssl_url}/http/\")\n        assert response.status_code == 200\n\n\n# ============================================================================\n# Error Handling Tests\n# ============================================================================\n\nclass TestErrorHandling:\n    \"\"\"Test error handling.\"\"\"\n\n    def test_invalid_json_body(self, http_client, gunicorn_url):\n        \"\"\"Test handling of invalid JSON body.\"\"\"\n        response = http_client.post(\n            f\"{gunicorn_url}/http/post-json\",\n            content=b\"not valid json\",\n            headers={\"Content-Type\": \"application/json\"}\n        )\n        assert response.status_code == 400\n        assert \"Invalid JSON\" in response.text\n\n    def test_method_not_allowed(self, http_client, gunicorn_url):\n        \"\"\"Test method not allowed response.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/http/post-json\")\n        assert response.status_code == 405\n"
  },
  {
    "path": "tests/docker/asgi_compliance/test_lifespan_compliance.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nLifespan compliance integration tests for ASGI.\n\nTests the ASGI lifespan protocol including startup, shutdown,\nand state sharing between lifespan and request handlers.\n\"\"\"\n\nimport pytest\n\npytestmark = [\n    pytest.mark.docker,\n    pytest.mark.asgi,\n    pytest.mark.lifespan,\n    pytest.mark.integration,\n]\n\n\n# ============================================================================\n# Basic Lifespan Tests\n# ============================================================================\n\nclass TestLifespanStartup:\n    \"\"\"Test lifespan startup behavior.\"\"\"\n\n    def test_startup_complete(self, http_client, gunicorn_url):\n        \"\"\"Test that lifespan startup completed.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/lifespan/state\")\n        assert response.status_code == 200\n        data = response.json()\n        # Check scope_state which is shared by main_app's lifespan handler\n        assert data[\"scope_state_available\"] is True\n        assert data[\"scope_state\"][\"main_app_started\"] is True\n\n    def test_startup_called(self, http_client, gunicorn_url):\n        \"\"\"Test that startup was called (via scope state).\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/lifespan/state\")\n        assert response.status_code == 200\n        data = response.json()\n        # Scope state indicates main_app handled lifespan startup\n        assert data[\"scope_state\"][\"main_app_started\"] is True\n\n    def test_startup_time_recorded(self, http_client, gunicorn_url):\n        \"\"\"Test that startup time was recorded.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/lifespan/state\")\n        assert response.status_code == 200\n        data = response.json()\n        # Startup time is recorded in scope_state by main_app\n        assert data[\"scope_state\"][\"startup_time\"] is not None\n\n    def test_health_after_startup(self, http_client, gunicorn_url):\n        \"\"\"Test health endpoint returns OK.\"\"\"\n        # The main health endpoint is at /health, lifespan's is at /lifespan/health\n        # but lifespan_app's health checks its own module_state which isn't set\n        # Use the main app health instead\n        response = http_client.get(f\"{gunicorn_url}/health\")\n        assert response.status_code == 200\n        assert response.text == \"OK\"\n\n\nclass TestLifespanInfo:\n    \"\"\"Test lifespan information endpoints.\"\"\"\n\n    def test_lifespan_info_endpoint(self, http_client, gunicorn_url):\n        \"\"\"Test lifespan info endpoint.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/lifespan/lifespan-info\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"lifespan_supported\"] is True\n        # scope_state_present indicates lifespan was handled (by main_app)\n        assert data[\"scope_state_present\"] is True\n\n    def test_uptime_tracking(self, http_client, gunicorn_url):\n        \"\"\"Test uptime is tracked via main app info endpoint.\"\"\"\n        # The lifespan_app's uptime won't be set since main_app handles lifespan\n        # Use the main app's /info endpoint instead\n        response = http_client.get(f\"{gunicorn_url}/info\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"uptime\"] is not None\n        assert data[\"uptime\"] >= 0\n\n\n# ============================================================================\n# State Sharing Tests\n# ============================================================================\n\nclass TestStateSharing:\n    \"\"\"Test state sharing between lifespan and request handlers.\"\"\"\n\n    def test_state_endpoint(self, http_client, gunicorn_url):\n        \"\"\"Test state endpoint returns state info.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/lifespan/state\")\n        assert response.status_code == 200\n        data = response.json()\n        assert \"module_state\" in data\n\n    def test_request_count_increments(self, http_client, gunicorn_url):\n        \"\"\"Test request count increments across requests.\"\"\"\n        # Make first request\n        response1 = http_client.get(f\"{gunicorn_url}/lifespan/counter\")\n        assert response1.status_code == 200\n        count1 = response1.json()[\"counter\"]\n\n        # Make second request\n        response2 = http_client.get(f\"{gunicorn_url}/lifespan/counter\")\n        assert response2.status_code == 200\n        count2 = response2.json()[\"counter\"]\n\n        # Counter should have incremented\n        assert count2 > count1\n\n\n\n# ============================================================================\n# Counter Tests\n# ============================================================================\n\nclass TestCounter:\n    \"\"\"Test counter functionality for state persistence.\"\"\"\n\n    def test_counter_endpoint(self, http_client, gunicorn_url):\n        \"\"\"Test counter endpoint.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/lifespan/counter\")\n        assert response.status_code == 200\n        data = response.json()\n        assert \"counter\" in data\n        assert \"source\" in data\n\n    def test_counter_increments_multiple_times(self, http_client, gunicorn_url):\n        \"\"\"Test counter increments across multiple requests.\"\"\"\n        counts = []\n        for _ in range(5):\n            response = http_client.get(f\"{gunicorn_url}/lifespan/counter\")\n            counts.append(response.json()[\"counter\"])\n\n        # Each count should be greater than the previous\n        for i in range(1, len(counts)):\n            assert counts[i] > counts[i - 1]\n\n\n# ============================================================================\n# Root and Basic Endpoint Tests\n# ============================================================================\n\nclass TestBasicEndpoints:\n    \"\"\"Test basic lifespan app endpoints.\"\"\"\n\n    def test_root_endpoint(self, http_client, gunicorn_url):\n        \"\"\"Test root endpoint.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/lifespan/\")\n        assert response.status_code == 200\n        assert response.text == \"Lifespan Test App\"\n\n    def test_not_found(self, http_client, gunicorn_url):\n        \"\"\"Test 404 for unknown path.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/lifespan/unknown-path\")\n        assert response.status_code == 404\n\n\n# ============================================================================\n# Proxy Lifespan Tests\n# ============================================================================\n\nclass TestProxyLifespan:\n    \"\"\"Test lifespan through nginx proxy.\"\"\"\n\n    def test_proxy_health(self, http_client, nginx_url):\n        \"\"\"Test health through proxy.\"\"\"\n        response = http_client.get(f\"{nginx_url}/health\")\n        assert response.status_code == 200\n        assert response.text == \"OK\"\n\n    def test_proxy_state(self, http_client, nginx_url):\n        \"\"\"Test state through proxy.\"\"\"\n        response = http_client.get(f\"{nginx_url}/lifespan/state\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"scope_state\"][\"main_app_started\"] is True\n\n    def test_proxy_counter(self, http_client, nginx_url):\n        \"\"\"Test counter through proxy.\"\"\"\n        response = http_client.get(f\"{nginx_url}/lifespan/counter\")\n        assert response.status_code == 200\n        data = response.json()\n        assert \"counter\" in data\n\n\n# ============================================================================\n# HTTPS Lifespan Tests\n# ============================================================================\n\n@pytest.mark.ssl\nclass TestHTTPSLifespan:\n    \"\"\"Test lifespan over HTTPS.\"\"\"\n\n    def test_https_health(self, http_client, gunicorn_ssl_url):\n        \"\"\"Test health over HTTPS.\"\"\"\n        response = http_client.get(f\"{gunicorn_ssl_url}/health\")\n        assert response.status_code == 200\n\n    def test_https_state(self, http_client, gunicorn_ssl_url):\n        \"\"\"Test state over HTTPS.\"\"\"\n        response = http_client.get(f\"{gunicorn_ssl_url}/lifespan/state\")\n        assert response.status_code == 200\n        data = response.json()\n        assert data[\"scope_state\"][\"main_app_started\"] is True\n\n    def test_https_proxy_health(self, http_client, nginx_ssl_url):\n        \"\"\"Test health through HTTPS proxy.\"\"\"\n        response = http_client.get(f\"{nginx_ssl_url}/health\")\n        assert response.status_code == 200\n\n\n# ============================================================================\n# Concurrent Access Tests\n# ============================================================================\n\n@pytest.mark.asyncio\nclass TestConcurrentLifespan:\n    \"\"\"Test concurrent access to lifespan state.\"\"\"\n\n    async def test_concurrent_counter_access(self, async_http_client_factory, gunicorn_url):\n        \"\"\"Test concurrent counter access.\"\"\"\n        import asyncio\n\n        async with await async_http_client_factory() as client:\n            async def get_counter():\n                response = await client.get(f\"{gunicorn_url}/lifespan/counter\")\n                return response.json()[\"counter\"]\n\n            # Run 10 concurrent requests\n            tasks = [get_counter() for _ in range(10)]\n            results = await asyncio.gather(*tasks)\n\n            # All should be valid integers\n            assert all(isinstance(r, int) for r in results)\n\n"
  },
  {
    "path": "tests/docker/asgi_compliance/test_streaming_compliance.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nStreaming compliance integration tests for ASGI.\n\nTests chunked transfer encoding, Server-Sent Events (SSE),\nand streaming response handling.\n\"\"\"\n\nimport json\nimport time\n\nimport pytest\n\npytestmark = [\n    pytest.mark.docker,\n    pytest.mark.asgi,\n    pytest.mark.streaming,\n    pytest.mark.integration,\n]\n\n\n# ============================================================================\n# Basic Streaming Tests\n# ============================================================================\n\nclass TestBasicStreaming:\n    \"\"\"Test basic streaming response functionality.\"\"\"\n\n    def test_streaming_endpoint(self, http_client, gunicorn_url):\n        \"\"\"Test basic streaming endpoint.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/stream/streaming\")\n        assert response.status_code == 200\n        assert \"Chunk\" in response.text\n\n    def test_streaming_multiple_chunks(self, http_client, gunicorn_url):\n        \"\"\"Test streaming returns multiple chunks.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/stream/streaming?chunks=5\")\n        assert response.status_code == 200\n        lines = response.text.strip().split(\"\\n\")\n        assert len(lines) == 5\n        assert \"Chunk 1 of 5\" in lines[0]\n        assert \"Chunk 5 of 5\" in lines[4]\n\n    def test_streaming_single_chunk(self, http_client, gunicorn_url):\n        \"\"\"Test streaming with single chunk.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/stream/streaming?chunks=1\")\n        assert response.status_code == 200\n        assert \"Chunk 1 of 1\" in response.text\n\n\nclass TestChunkedStreaming:\n    \"\"\"Test chunked streaming with the streaming client.\"\"\"\n\n    def test_stream_chunks_received(self, streaming_client, gunicorn_url):\n        \"\"\"Test that chunks are received incrementally.\"\"\"\n        chunks = list(streaming_client.stream_chunks(f\"{gunicorn_url}/stream/streaming?chunks=3\"))\n        assert len(chunks) >= 1\n        full_content = b\"\".join(chunks).decode(\"utf-8\")\n        assert \"Chunk 1\" in full_content\n        assert \"Chunk 3\" in full_content\n\n    def test_stream_variable_chunk_sizes(self, streaming_client, gunicorn_url):\n        \"\"\"Test streaming with variable chunk sizes.\"\"\"\n        chunks = list(streaming_client.stream_chunks(\n            f\"{gunicorn_url}/stream/chunked?sizes=100,500,200\"\n        ))\n        total_size = sum(len(c) for c in chunks)\n        assert total_size == 800  # 100 + 500 + 200\n\n    def test_stream_lines(self, streaming_client, gunicorn_url):\n        \"\"\"Test streaming response line by line.\"\"\"\n        lines = list(streaming_client.stream_lines(f\"{gunicorn_url}/stream/streaming?chunks=5\"))\n        non_empty_lines = [l for l in lines if l.strip()]\n        assert len(non_empty_lines) == 5\n\n\n# ============================================================================\n# Server-Sent Events (SSE) Tests\n# ============================================================================\n\nclass TestServerSentEvents:\n    \"\"\"Test Server-Sent Events functionality.\"\"\"\n\n    def test_sse_content_type(self, http_client, gunicorn_url):\n        \"\"\"Test SSE has correct content type.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/stream/sse?events=1\")\n        assert response.status_code == 200\n        assert \"text/event-stream\" in response.headers.get(\"content-type\", \"\")\n\n    def test_sse_event_format(self, http_client, gunicorn_url):\n        \"\"\"Test SSE event format.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/stream/sse?events=3&delay=0.1\")\n        assert response.status_code == 200\n\n        # Parse SSE events\n        events = []\n        for event_text in response.text.split(\"\\n\\n\"):\n            if event_text.strip():\n                event = {}\n                for line in event_text.strip().split(\"\\n\"):\n                    if line.startswith(\"id: \"):\n                        event[\"id\"] = line[4:]\n                    elif line.startswith(\"event: \"):\n                        event[\"event\"] = line[7:]\n                    elif line.startswith(\"data: \"):\n                        event[\"data\"] = line[6:]\n                if event:\n                    events.append(event)\n\n        assert len(events) == 3\n        assert events[0][\"id\"] == \"1\"\n        assert events[0][\"event\"] == \"message\"\n\n    def test_sse_data_is_json(self, http_client, gunicorn_url):\n        \"\"\"Test SSE data contains valid JSON.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/stream/sse?events=1\")\n        assert response.status_code == 200\n\n        # Find data line\n        for line in response.text.split(\"\\n\"):\n            if line.startswith(\"data: \"):\n                data = json.loads(line[6:])\n                assert \"id\" in data\n                assert \"timestamp\" in data\n                break\n\n    def test_sse_multiple_events(self, http_client, gunicorn_url):\n        \"\"\"Test receiving multiple SSE events.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/stream/sse?events=5&delay=0.05\")\n        assert response.status_code == 200\n\n        # Count events by counting \"id:\" lines\n        id_count = response.text.count(\"id: \")\n        assert id_count == 5\n\n\nclass TestSSEClient:\n    \"\"\"Test SSE with dedicated SSE client.\"\"\"\n\n    def test_sse_client_receives_events(self, sse_client, gunicorn_url):\n        \"\"\"Test SSE client receives events.\"\"\"\n        events = list(sse_client.stream(f\"{gunicorn_url}/stream/sse?events=3&delay=0.1\"))\n        assert len(events) == 3\n\n    def test_sse_client_parses_data(self, sse_client, gunicorn_url):\n        \"\"\"Test SSE client parses event data.\"\"\"\n        events = list(sse_client.stream(f\"{gunicorn_url}/stream/sse?events=2&delay=0.1\"))\n\n        for event in events:\n            assert event[\"event\"] == \"message\"\n            assert event[\"data\"] is not None\n            data = json.loads(event[\"data\"])\n            assert \"id\" in data\n\n\n# ============================================================================\n# NDJSON Streaming Tests\n# ============================================================================\n\nclass TestNDJSONStreaming:\n    \"\"\"Test Newline-Delimited JSON streaming.\"\"\"\n\n    def test_ndjson_content_type(self, http_client, gunicorn_url):\n        \"\"\"Test NDJSON has correct content type.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/stream/ndjson?records=1\")\n        assert response.status_code == 200\n        assert \"application/x-ndjson\" in response.headers.get(\"content-type\", \"\")\n\n    def test_ndjson_format(self, http_client, gunicorn_url):\n        \"\"\"Test NDJSON line format.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/stream/ndjson?records=3&delay=0\")\n        assert response.status_code == 200\n\n        lines = response.text.strip().split(\"\\n\")\n        assert len(lines) == 3\n\n        for i, line in enumerate(lines):\n            record = json.loads(line)\n            assert record[\"id\"] == i + 1\n            assert \"timestamp\" in record\n            assert \"data\" in record\n\n    def test_ndjson_streaming(self, streaming_client, gunicorn_url):\n        \"\"\"Test NDJSON received as stream.\"\"\"\n        lines = list(streaming_client.stream_lines(\n            f\"{gunicorn_url}/stream/ndjson?records=5&delay=0.1\"\n        ))\n        non_empty = [l for l in lines if l.strip()]\n        assert len(non_empty) == 5\n\n\n# ============================================================================\n# Slow Streaming Tests\n# ============================================================================\n\nclass TestSlowStreaming:\n    \"\"\"Test slow/delayed streaming responses.\"\"\"\n\n    def test_slow_stream_completes(self, http_client, gunicorn_url):\n        \"\"\"Test slow stream eventually completes.\"\"\"\n        start = time.time()\n        response = http_client.get(f\"{gunicorn_url}/stream/slow-stream?chunks=3&delay=0.2\")\n        elapsed = time.time() - start\n\n        assert response.status_code == 200\n        assert elapsed >= 0.4  # At least 2 delays\n        assert \"Slow chunk 3/3\" in response.text\n\n    def test_slow_stream_chunks_timed(self, streaming_client, gunicorn_url):\n        \"\"\"Test slow stream chunks arrive at intervals.\"\"\"\n        chunks = []\n        times = []\n\n        for chunk in streaming_client.stream_chunks(\n            f\"{gunicorn_url}/stream/slow-stream?chunks=3&delay=0.3\"\n        ):\n            chunks.append(chunk)\n            times.append(time.time())\n\n        # Should have some time between chunks\n        if len(times) >= 2:\n            assert times[-1] - times[0] >= 0.3\n\n\n# ============================================================================\n# Large Streaming Tests\n# ============================================================================\n\nclass TestLargeStreaming:\n    \"\"\"Test large streaming responses.\"\"\"\n\n    def test_large_stream_size(self, http_client, gunicorn_url):\n        \"\"\"Test large streaming response has correct size.\"\"\"\n        size = 1024 * 1024  # 1MB\n        response = http_client.get(f\"{gunicorn_url}/stream/large-stream?size={size}\")\n        assert response.status_code == 200\n        assert len(response.content) == size\n\n    def test_large_stream_chunked(self, streaming_client, gunicorn_url):\n        \"\"\"Test large streaming response arrives in chunks.\"\"\"\n        size = 512 * 1024  # 512KB\n        chunk_size = 64 * 1024  # 64KB chunks\n\n        chunks = list(streaming_client.stream_chunks(\n            f\"{gunicorn_url}/stream/large-stream?size={size}&chunk={chunk_size}\"\n        ))\n\n        total_size = sum(len(c) for c in chunks)\n        assert total_size == size\n        # Should have multiple chunks\n        assert len(chunks) >= 2\n\n\n# ============================================================================\n# Echo Stream Tests\n# ============================================================================\n\nclass TestEchoStreaming:\n    \"\"\"Test streaming echo endpoint.\"\"\"\n\n    def test_echo_stream_response(self, http_client, gunicorn_url):\n        \"\"\"Test echo stream returns chunked response.\"\"\"\n        body = b\"Hello, streaming world!\"\n        response = http_client.post(\n            f\"{gunicorn_url}/stream/echo-stream\",\n            content=body\n        )\n        assert response.status_code == 200\n        assert b\"chunk\" in response.content.lower()\n\n    def test_echo_stream_large_body(self, http_client, gunicorn_url):\n        \"\"\"Test echo stream with large body.\"\"\"\n        body = b\"x\" * (100 * 1024)  # 100KB\n        response = http_client.post(\n            f\"{gunicorn_url}/stream/echo-stream\",\n            content=body\n        )\n        assert response.status_code == 200\n        assert b\"Total chunks received\" in response.content\n\n\n# ============================================================================\n# Transfer-Encoding Tests\n# ============================================================================\n\nclass TestTransferEncoding:\n    \"\"\"Test Transfer-Encoding header handling.\"\"\"\n\n    def test_chunked_encoding_header(self, http_client, gunicorn_url):\n        \"\"\"Test response uses chunked transfer encoding.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/stream/streaming?chunks=3\")\n        assert response.status_code == 200\n        # Note: httpx may decompress/dechunk, so we check the response completed\n        assert \"Chunk\" in response.text\n\n    def test_no_content_length_in_stream(self, http_client, gunicorn_url):\n        \"\"\"Test streaming response may not have Content-Length.\"\"\"\n        # This is implementation-dependent; chunked encoding doesn't require it\n        response = http_client.get(f\"{gunicorn_url}/stream/streaming?chunks=3\")\n        assert response.status_code == 200\n        # The response should complete successfully regardless\n\n\n# ============================================================================\n# Proxy Streaming Tests\n# ============================================================================\n\nclass TestProxyStreaming:\n    \"\"\"Test streaming through nginx proxy.\"\"\"\n\n    def test_proxy_streaming(self, http_client, nginx_url):\n        \"\"\"Test streaming through proxy.\"\"\"\n        response = http_client.get(f\"{nginx_url}/stream/streaming?chunks=3\")\n        assert response.status_code == 200\n        assert \"Chunk\" in response.text\n\n    def test_proxy_sse(self, http_client, nginx_url):\n        \"\"\"Test SSE through proxy.\"\"\"\n        response = http_client.get(f\"{nginx_url}/stream/sse?events=3&delay=0.1\")\n        assert response.status_code == 200\n        assert \"text/event-stream\" in response.headers.get(\"content-type\", \"\")\n        assert \"id: 1\" in response.text\n\n    def test_proxy_large_stream(self, http_client, nginx_url):\n        \"\"\"Test large streaming through proxy.\"\"\"\n        size = 512 * 1024\n        response = http_client.get(f\"{nginx_url}/stream/large-stream?size={size}\")\n        assert response.status_code == 200\n        assert len(response.content) == size\n\n    def test_proxy_slow_stream(self, streaming_client, nginx_url):\n        \"\"\"Test slow streaming through proxy.\"\"\"\n        chunks = list(streaming_client.stream_chunks(\n            f\"{nginx_url}/stream/slow-stream?chunks=3&delay=0.2\"\n        ))\n        full_content = b\"\".join(chunks).decode(\"utf-8\")\n        assert \"Slow chunk 3/3\" in full_content\n\n\n# ============================================================================\n# HTTPS Streaming Tests\n# ============================================================================\n\n@pytest.mark.ssl\nclass TestHTTPSStreaming:\n    \"\"\"Test streaming over HTTPS.\"\"\"\n\n    def test_https_streaming(self, http_client, gunicorn_ssl_url):\n        \"\"\"Test streaming over HTTPS.\"\"\"\n        response = http_client.get(f\"{gunicorn_ssl_url}/stream/streaming?chunks=3\")\n        assert response.status_code == 200\n        assert \"Chunk\" in response.text\n\n    def test_https_sse(self, http_client, gunicorn_ssl_url):\n        \"\"\"Test SSE over HTTPS.\"\"\"\n        response = http_client.get(f\"{gunicorn_ssl_url}/stream/sse?events=2&delay=0.1\")\n        assert response.status_code == 200\n        assert \"id: 1\" in response.text\n\n    def test_https_proxy_streaming(self, http_client, nginx_ssl_url):\n        \"\"\"Test streaming through HTTPS proxy.\"\"\"\n        response = http_client.get(f\"{nginx_ssl_url}/stream/streaming?chunks=3\")\n        assert response.status_code == 200\n\n\n# ============================================================================\n# Async Streaming Tests\n# ============================================================================\n\n@pytest.mark.asyncio\nclass TestAsyncStreaming:\n    \"\"\"Test streaming with async client.\"\"\"\n\n    async def test_async_streaming(self, async_http_client_factory, gunicorn_url):\n        \"\"\"Test async streaming.\"\"\"\n        async with await async_http_client_factory() as client:\n            response = await client.get(f\"{gunicorn_url}/stream/streaming?chunks=3\")\n            assert response.status_code == 200\n            assert \"Chunk\" in response.text\n\n    async def test_async_stream_chunks(self, async_http_client_factory, gunicorn_url):\n        \"\"\"Test async streaming with iter_bytes.\"\"\"\n        async with await async_http_client_factory() as client:\n            chunks = []\n            async with client.stream(\"GET\", f\"{gunicorn_url}/stream/streaming?chunks=5\") as response:\n                async for chunk in response.aiter_bytes():\n                    if chunk:\n                        chunks.append(chunk)\n\n            full_content = b\"\".join(chunks).decode(\"utf-8\")\n            assert \"Chunk 5 of 5\" in full_content\n\n    async def test_async_sse(self, async_http_client_factory, gunicorn_url):\n        \"\"\"Test async SSE streaming.\"\"\"\n        async with await async_http_client_factory() as client:\n            events = []\n            async with client.stream(\n                \"GET\",\n                f\"{gunicorn_url}/stream/sse?events=3&delay=0.1\"\n            ) as response:\n                buffer = \"\"\n                async for chunk in response.aiter_text():\n                    buffer += chunk\n                    while \"\\n\\n\" in buffer:\n                        event_text, buffer = buffer.split(\"\\n\\n\", 1)\n                        if event_text.strip():\n                            events.append(event_text)\n\n            assert len(events) == 3\n\n\n# ============================================================================\n# Edge Cases\n# ============================================================================\n\nclass TestStreamingEdgeCases:\n    \"\"\"Test streaming edge cases.\"\"\"\n\n    def test_empty_stream(self, http_client, gunicorn_url):\n        \"\"\"Test streaming with zero chunks.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/stream/streaming?chunks=0\")\n        assert response.status_code == 200\n        # Should complete without error\n\n    def test_single_byte_chunks(self, streaming_client, gunicorn_url):\n        \"\"\"Test with very small chunks.\"\"\"\n        response_chunks = list(streaming_client.stream_chunks(\n            f\"{gunicorn_url}/stream/chunked?sizes=1,1,1,1,1\"\n        ))\n        total_size = sum(len(c) for c in response_chunks)\n        assert total_size == 5\n\n    def test_sse_no_delay(self, http_client, gunicorn_url):\n        \"\"\"Test SSE with no delay between events.\"\"\"\n        response = http_client.get(f\"{gunicorn_url}/stream/sse?events=10&delay=0\")\n        assert response.status_code == 200\n        assert response.text.count(\"id:\") == 10\n"
  },
  {
    "path": "tests/docker/asgi_compliance/test_websocket_compliance.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nWebSocket compliance integration tests for ASGI.\n\nTests RFC 6455 WebSocket protocol compliance including handshake,\nmessaging, close codes, and subprotocol negotiation.\n\"\"\"\n\nimport asyncio\nimport json\n\nimport pytest\n\npytestmark = [\n    pytest.mark.docker,\n    pytest.mark.asgi,\n    pytest.mark.websocket,\n    pytest.mark.integration,\n]\n\n\n# ============================================================================\n# WebSocket Handshake Tests\n# ============================================================================\n\n@pytest.mark.asyncio\nclass TestWebSocketHandshake:\n    \"\"\"Test WebSocket handshake and connection establishment.\"\"\"\n\n    async def test_basic_connection(self, websocket_connect, gunicorn_url):\n        \"\"\"Test basic WebSocket connection.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/echo\"\n        async with await websocket_connect(ws_url) as ws:\n            # Connection successful - verify by sending a message\n            await ws.send(\"test\")\n            response = await ws.recv()\n            assert response == \"test\"\n\n    async def test_echo_after_connect(self, websocket_connect, gunicorn_url):\n        \"\"\"Test sending message after connection.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/echo\"\n        async with await websocket_connect(ws_url) as ws:\n            await ws.send(\"hello\")\n            response = await ws.recv()\n            assert response == \"hello\"\n\n    async def test_connection_path_preserved(self, websocket_connect, gunicorn_url):\n        \"\"\"Test that connection path is preserved in scope.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/scope\"\n        async with await websocket_connect(ws_url) as ws:\n            response = await ws.recv()\n            scope = json.loads(response)\n            assert scope[\"path\"] == \"/ws/scope\"\n\n    async def test_connection_with_query_string(self, websocket_connect, gunicorn_url):\n        \"\"\"Test connection with query string.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/scope?foo=bar&baz=qux\"\n        async with await websocket_connect(ws_url) as ws:\n            response = await ws.recv()\n            scope = json.loads(response)\n            assert \"foo=bar\" in scope[\"query_string\"]\n            assert \"baz=qux\" in scope[\"query_string\"]\n\n\n# ============================================================================\n# Text Message Tests\n# ============================================================================\n\n@pytest.mark.asyncio\nclass TestTextMessages:\n    \"\"\"Test WebSocket text message handling.\"\"\"\n\n    async def test_echo_text(self, websocket_connect, gunicorn_url):\n        \"\"\"Test echoing text message.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/echo\"\n        async with await websocket_connect(ws_url) as ws:\n            await ws.send(\"Hello, WebSocket!\")\n            response = await ws.recv()\n            assert response == \"Hello, WebSocket!\"\n\n    async def test_echo_unicode(self, websocket_connect, gunicorn_url):\n        \"\"\"Test echoing unicode text.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/echo\"\n        async with await websocket_connect(ws_url) as ws:\n            message = \"Hello \\u4e16\\u754c! \\U0001f600\"  # Hello World in Chinese + emoji\n            await ws.send(message)\n            response = await ws.recv()\n            assert response == message\n\n    async def test_echo_empty_string(self, websocket_connect, gunicorn_url):\n        \"\"\"Test echoing empty string.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/echo\"\n        async with await websocket_connect(ws_url) as ws:\n            await ws.send(\"\")\n            response = await ws.recv()\n            assert response == \"\"\n\n    async def test_multiple_messages(self, websocket_connect, gunicorn_url):\n        \"\"\"Test sending multiple messages.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/echo\"\n        async with await websocket_connect(ws_url) as ws:\n            messages = [\"first\", \"second\", \"third\"]\n            for msg in messages:\n                await ws.send(msg)\n                response = await ws.recv()\n                assert response == msg\n\n    async def test_rapid_messages(self, websocket_connect, gunicorn_url):\n        \"\"\"Test sending messages rapidly.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/echo\"\n        async with await websocket_connect(ws_url) as ws:\n            count = 100\n            for i in range(count):\n                await ws.send(f\"message {i}\")\n\n            for i in range(count):\n                response = await ws.recv()\n                assert f\"message {i}\" == response\n\n\n# ============================================================================\n# Binary Message Tests\n# ============================================================================\n\n@pytest.mark.asyncio\nclass TestBinaryMessages:\n    \"\"\"Test WebSocket binary message handling.\"\"\"\n\n    async def test_echo_binary(self, websocket_connect, gunicorn_url):\n        \"\"\"Test echoing binary message.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/echo-binary\"\n        async with await websocket_connect(ws_url) as ws:\n            data = b\"\\x00\\x01\\x02\\x03\\x04\\x05\"\n            await ws.send(data)\n            response = await ws.recv()\n            assert response == data\n\n    async def test_echo_binary_large(self, websocket_connect, gunicorn_url):\n        \"\"\"Test echoing larger binary message.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/echo-binary\"\n        async with await websocket_connect(ws_url) as ws:\n            data = bytes(range(256)) * 100  # 25.6KB\n            await ws.send(data)\n            response = await ws.recv()\n            assert response == data\n\n    async def test_text_to_binary_conversion(self, websocket_connect, gunicorn_url):\n        \"\"\"Test text converted to binary in binary endpoint.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/echo-binary\"\n        async with await websocket_connect(ws_url) as ws:\n            await ws.send(\"hello\")\n            response = await ws.recv()\n            assert response == b\"hello\"\n\n\n# ============================================================================\n# Subprotocol Negotiation Tests\n# ============================================================================\n\n@pytest.mark.asyncio\nclass TestSubprotocols:\n    \"\"\"Test WebSocket subprotocol negotiation.\"\"\"\n\n    async def test_single_subprotocol(self, websocket_connect, gunicorn_url):\n        \"\"\"Test single subprotocol negotiation.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/subprotocol\"\n        async with await websocket_connect(ws_url, subprotocols=[\"json\"]) as ws:\n            response = await ws.recv()\n            data = json.loads(response)\n            assert data[\"selected\"] == \"json\"\n            assert data[\"requested\"] == [\"json\"]\n\n    async def test_multiple_subprotocols(self, websocket_connect, gunicorn_url):\n        \"\"\"Test multiple subprotocol negotiation.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/subprotocol\"\n        async with await websocket_connect(ws_url, subprotocols=[\"wamp\", \"json\"]) as ws:\n            response = await ws.recv()\n            data = json.loads(response)\n            # Server prefers json over wamp\n            assert data[\"selected\"] == \"json\"\n            assert set(data[\"requested\"]) == {\"wamp\", \"json\"}\n\n    async def test_preferred_subprotocol(self, websocket_connect, gunicorn_url):\n        \"\"\"Test server-preferred subprotocol selection.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/subprotocol\"\n        async with await websocket_connect(ws_url, subprotocols=[\"json\", \"graphql-ws\"]) as ws:\n            response = await ws.recv()\n            data = json.loads(response)\n            # Server prefers graphql-ws\n            assert data[\"selected\"] == \"graphql-ws\"\n\n    async def test_no_subprotocol(self, websocket_connect, gunicorn_url):\n        \"\"\"Test connection without subprotocol.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/subprotocol\"\n        async with await websocket_connect(ws_url) as ws:\n            response = await ws.recv()\n            data = json.loads(response)\n            assert data[\"selected\"] is None\n            assert data[\"requested\"] == []\n\n\n# ============================================================================\n# Close Code Tests\n# ============================================================================\n\n@pytest.mark.asyncio\nclass TestCloseCodes:\n    \"\"\"Test WebSocket close code handling.\"\"\"\n\n    async def test_normal_close(self, websocket_connect, gunicorn_url):\n        \"\"\"Test normal close (1000).\"\"\"\n        websockets = pytest.importorskip(\"websockets\")\n\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/close?code=1000\"\n        async with await websocket_connect(ws_url) as ws:\n            try:\n                await ws.recv()\n            except websockets.exceptions.ConnectionClosed as e:\n                assert e.code == 1000\n\n    async def test_going_away_close(self, websocket_connect, gunicorn_url):\n        \"\"\"Test going away close (1001).\"\"\"\n        websockets = pytest.importorskip(\"websockets\")\n\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/close?code=1001\"\n        async with await websocket_connect(ws_url) as ws:\n            try:\n                await ws.recv()\n            except websockets.exceptions.ConnectionClosed as e:\n                assert e.code == 1001\n\n    async def test_protocol_error_close(self, websocket_connect, gunicorn_url):\n        \"\"\"Test protocol error close (1002).\"\"\"\n        websockets = pytest.importorskip(\"websockets\")\n\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/close?code=1002\"\n        async with await websocket_connect(ws_url) as ws:\n            try:\n                await ws.recv()\n            except websockets.exceptions.ConnectionClosed as e:\n                assert e.code == 1002\n\n    async def test_close_with_reason(self, websocket_connect, gunicorn_url):\n        \"\"\"Test close with reason message.\"\"\"\n        websockets = pytest.importorskip(\"websockets\")\n\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/close?code=1000&reason=goodbye\"\n        async with await websocket_connect(ws_url) as ws:\n            try:\n                await ws.recv()\n            except websockets.exceptions.ConnectionClosed as e:\n                assert e.code == 1000\n                assert e.reason == \"goodbye\"\n\n    async def test_application_close_code(self, websocket_connect, gunicorn_url):\n        \"\"\"Test application-defined close code (4000+).\"\"\"\n        websockets = pytest.importorskip(\"websockets\")\n\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/close?code=4001\"\n        async with await websocket_connect(ws_url) as ws:\n            try:\n                await ws.recv()\n            except websockets.exceptions.ConnectionClosed as e:\n                assert e.code == 4001\n\n\n# ============================================================================\n# Connection Rejection Tests\n# ============================================================================\n\n@pytest.mark.asyncio\nclass TestConnectionRejection:\n    \"\"\"Test WebSocket connection rejection.\"\"\"\n\n    async def test_reject_connection(self, websocket_connect, gunicorn_url):\n        \"\"\"Test connection rejection.\"\"\"\n        websockets = pytest.importorskip(\"websockets\")\n\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/reject\"\n        # websockets v16+ raises InvalidStatus, older versions raise InvalidStatusCode\n        with pytest.raises((websockets.exceptions.InvalidStatus, Exception)):\n            async with await websocket_connect(ws_url):\n                pass\n\n\n# ============================================================================\n# WebSocket Scope Tests\n# ============================================================================\n\n@pytest.mark.asyncio\nclass TestWebSocketScope:\n    \"\"\"Test WebSocket ASGI scope correctness.\"\"\"\n\n    async def test_scope_type(self, websocket_connect, gunicorn_url):\n        \"\"\"Test scope type is 'websocket'.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/scope\"\n        async with await websocket_connect(ws_url) as ws:\n            response = await ws.recv()\n            scope = json.loads(response)\n            assert scope[\"type\"] == \"websocket\"\n\n    async def test_scope_asgi_version(self, websocket_connect, gunicorn_url):\n        \"\"\"Test scope has ASGI version.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/scope\"\n        async with await websocket_connect(ws_url) as ws:\n            response = await ws.recv()\n            scope = json.loads(response)\n            assert \"asgi\" in scope\n            assert \"version\" in scope[\"asgi\"]\n\n    async def test_scope_http_version(self, websocket_connect, gunicorn_url):\n        \"\"\"Test scope has HTTP version.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/scope\"\n        async with await websocket_connect(ws_url) as ws:\n            response = await ws.recv()\n            scope = json.loads(response)\n            assert scope[\"http_version\"] in [\"1.0\", \"1.1\", \"2\"]\n\n    async def test_scope_scheme(self, websocket_connect, gunicorn_url):\n        \"\"\"Test scope scheme is 'ws'.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/scope\"\n        async with await websocket_connect(ws_url) as ws:\n            response = await ws.recv()\n            scope = json.loads(response)\n            assert scope[\"scheme\"] == \"ws\"\n\n    async def test_scope_server(self, websocket_connect, gunicorn_url):\n        \"\"\"Test scope has server info.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/scope\"\n        async with await websocket_connect(ws_url) as ws:\n            response = await ws.recv()\n            scope = json.loads(response)\n            assert scope[\"server\"] is not None\n            assert len(scope[\"server\"]) == 2  # (host, port)\n\n    async def test_scope_client(self, websocket_connect, gunicorn_url):\n        \"\"\"Test scope has client info.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/scope\"\n        async with await websocket_connect(ws_url) as ws:\n            response = await ws.recv()\n            scope = json.loads(response)\n            assert scope[\"client\"] is not None\n            assert len(scope[\"client\"]) == 2  # (host, port)\n\n    async def test_scope_headers(self, websocket_connect, gunicorn_url):\n        \"\"\"Test scope has headers.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/scope\"\n        async with await websocket_connect(\n            ws_url,\n            additional_headers={\"X-Custom-Header\": \"test-value\"}\n        ) as ws:\n            response = await ws.recv()\n            scope = json.loads(response)\n            headers = {name.lower(): value for name, value in scope[\"headers\"]}\n            assert \"x-custom-header\" in headers\n            assert headers[\"x-custom-header\"] == \"test-value\"\n\n\n# ============================================================================\n# Large Message Tests\n# ============================================================================\n\n@pytest.mark.asyncio\nclass TestLargeMessages:\n    \"\"\"Test large WebSocket message handling.\"\"\"\n\n    async def test_receive_large_message(self, websocket_connect, gunicorn_url):\n        \"\"\"Test receiving large message from server.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/large?size=65536\"\n        async with await websocket_connect(ws_url) as ws:\n            response = await ws.recv()\n            assert len(response) == 65536\n            assert response == \"x\" * 65536\n\n    async def test_send_large_message(self, websocket_connect, gunicorn_url):\n        \"\"\"Test sending large message to server.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/large?size=1024\"\n        async with await websocket_connect(ws_url) as ws:\n            # First receive server's large message\n            _ = await ws.recv()\n\n            # Send our large message\n            large_data = \"y\" * 100000\n            await ws.send(large_data)\n\n            response = await ws.recv()\n            data = json.loads(response)\n            assert data[\"received_length\"] == 100000\n\n    async def test_various_sizes(self, websocket_connect, gunicorn_url):\n        \"\"\"Test various message sizes.\"\"\"\n        sizes = [1, 100, 1000, 10000, 50000]\n\n        for size in sizes:\n            ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + f\"/ws/large?size={size}\"\n            async with await websocket_connect(ws_url) as ws:\n                response = await ws.recv()\n                assert len(response) == size, f\"Expected {size}, got {len(response)}\"\n\n\n# ============================================================================\n# Broadcast/Multiple Message Tests\n# ============================================================================\n\n@pytest.mark.asyncio\nclass TestBroadcast:\n    \"\"\"Test broadcast-style multiple message sending.\"\"\"\n\n    async def test_broadcast_default_count(self, websocket_connect, gunicorn_url):\n        \"\"\"Test broadcast with default count (3).\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/broadcast\"\n        async with await websocket_connect(ws_url) as ws:\n            await ws.send(\"test message\")\n\n            responses = []\n            for _ in range(3):\n                response = await ws.recv()\n                responses.append(json.loads(response))\n\n            assert len(responses) == 3\n            for i, resp in enumerate(responses):\n                assert resp[\"copy\"] == i + 1\n                assert resp[\"of\"] == 3\n                assert resp[\"message\"] == \"test message\"\n\n    async def test_broadcast_custom_count(self, websocket_connect, gunicorn_url):\n        \"\"\"Test broadcast with custom count.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/broadcast?count=5\"\n        async with await websocket_connect(ws_url) as ws:\n            await ws.send(\"hello\")\n\n            responses = []\n            for _ in range(5):\n                response = await ws.recv()\n                responses.append(json.loads(response))\n\n            assert len(responses) == 5\n\n\n# ============================================================================\n# Delayed Response Tests\n# ============================================================================\n\n@pytest.mark.asyncio\nclass TestDelayedResponses:\n    \"\"\"Test WebSocket delayed responses.\"\"\"\n\n    async def test_delayed_response(self, websocket_connect, gunicorn_url):\n        \"\"\"Test delayed response.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/delay?seconds=0.5\"\n        async with await websocket_connect(ws_url) as ws:\n            import time\n            start = time.time()\n            await ws.send(\"ping\")\n            response = await asyncio.wait_for(ws.recv(), timeout=5.0)\n            elapsed = time.time() - start\n\n            assert elapsed >= 0.4  # Allow some tolerance\n            data = json.loads(response)\n            assert data[\"delayed_by\"] == 0.5\n            assert data[\"message\"] == \"ping\"\n\n    async def test_minimal_delay(self, websocket_connect, gunicorn_url):\n        \"\"\"Test with minimal delay.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/delay?seconds=0.1\"\n        async with await websocket_connect(ws_url) as ws:\n            await ws.send(\"quick\")\n            response = await asyncio.wait_for(ws.recv(), timeout=5.0)\n            data = json.loads(response)\n            assert data[\"delayed_by\"] == 0.1\n\n\n# ============================================================================\n# Fragmented Message Tests\n# ============================================================================\n\n@pytest.mark.asyncio\nclass TestFragmentedMessages:\n    \"\"\"Test fragmented WebSocket message handling.\"\"\"\n\n    async def test_fragmented_endpoint(self, websocket_connect, gunicorn_url):\n        \"\"\"Test fragmented message info endpoint.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/fragmented\"\n        async with await websocket_connect(ws_url) as ws:\n            # First receive info message\n            info = await ws.recv()\n            data = json.loads(info)\n            assert \"info\" in data\n\n    async def test_message_reassembly(self, websocket_connect, gunicorn_url):\n        \"\"\"Test that messages are reassembled correctly.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/fragmented\"\n        async with await websocket_connect(ws_url) as ws:\n            # Skip info message\n            await ws.recv()\n\n            # Send message\n            await ws.send(\"complete message\")\n            response = await ws.recv()\n            data = json.loads(response)\n\n            assert data[\"received\"] == \"complete message\"\n            assert data[\"length\"] == len(\"complete message\")\n            assert data[\"type\"] == \"text\"\n\n\n# ============================================================================\n# Proxy WebSocket Tests\n# ============================================================================\n\n@pytest.mark.asyncio\nclass TestProxyWebSocket:\n    \"\"\"Test WebSocket through nginx proxy.\"\"\"\n\n    async def test_proxy_echo(self, websocket_connect, nginx_url):\n        \"\"\"Test echo through proxy.\"\"\"\n        ws_url = nginx_url.replace(\"http://\", \"ws://\") + \"/ws/echo\"\n        async with await websocket_connect(ws_url) as ws:\n            await ws.send(\"proxied message\")\n            response = await ws.recv()\n            assert response == \"proxied message\"\n\n    async def test_proxy_binary(self, websocket_connect, nginx_url):\n        \"\"\"Test binary echo through proxy.\"\"\"\n        ws_url = nginx_url.replace(\"http://\", \"ws://\") + \"/ws/echo-binary\"\n        async with await websocket_connect(ws_url) as ws:\n            data = b\"\\x00\\x01\\x02\\x03\"\n            await ws.send(data)\n            response = await ws.recv()\n            assert response == data\n\n    async def test_proxy_subprotocol(self, websocket_connect, nginx_url):\n        \"\"\"Test subprotocol through proxy.\"\"\"\n        ws_url = nginx_url.replace(\"http://\", \"ws://\") + \"/ws/subprotocol\"\n        async with await websocket_connect(ws_url, subprotocols=[\"json\"]) as ws:\n            response = await ws.recv()\n            data = json.loads(response)\n            assert data[\"selected\"] == \"json\"\n\n    async def test_proxy_scope(self, websocket_connect, nginx_url):\n        \"\"\"Test scope through proxy.\"\"\"\n        ws_url = nginx_url.replace(\"http://\", \"ws://\") + \"/ws/scope\"\n        async with await websocket_connect(ws_url) as ws:\n            response = await ws.recv()\n            scope = json.loads(response)\n            assert scope[\"type\"] == \"websocket\"\n            assert scope[\"path\"] == \"/ws/scope\"\n\n\n# ============================================================================\n# HTTPS WebSocket Tests\n# ============================================================================\n\n@pytest.mark.ssl\n@pytest.mark.asyncio\nclass TestSecureWebSocket:\n    \"\"\"Test WebSocket over SSL/TLS.\"\"\"\n\n    async def test_wss_connection(self, websocket_connect, gunicorn_ssl_url):\n        \"\"\"Test WSS connection.\"\"\"\n        ws_url = gunicorn_ssl_url.replace(\"https://\", \"wss://\") + \"/ws/echo\"\n        async with await websocket_connect(ws_url) as ws:\n            await ws.send(\"secure message\")\n            response = await ws.recv()\n            assert response == \"secure message\"\n\n    async def test_wss_scope_scheme(self, websocket_connect, gunicorn_ssl_url):\n        \"\"\"Test WSS scope has correct scheme.\"\"\"\n        ws_url = gunicorn_ssl_url.replace(\"https://\", \"wss://\") + \"/ws/scope\"\n        async with await websocket_connect(ws_url) as ws:\n            response = await ws.recv()\n            scope = json.loads(response)\n            assert scope[\"scheme\"] == \"wss\"\n\n    async def test_wss_through_proxy(self, websocket_connect, nginx_ssl_url):\n        \"\"\"Test WSS through nginx proxy.\"\"\"\n        ws_url = nginx_ssl_url.replace(\"https://\", \"wss://\") + \"/ws/echo\"\n        async with await websocket_connect(ws_url) as ws:\n            await ws.send(\"secure proxied\")\n            response = await ws.recv()\n            assert response == \"secure proxied\"\n\n\n# ============================================================================\n# Concurrent Connection Tests\n# ============================================================================\n\n@pytest.mark.asyncio\nclass TestConcurrentConnections:\n    \"\"\"Test concurrent WebSocket connections.\"\"\"\n\n    async def test_multiple_concurrent_connections(self, websocket_connect, gunicorn_url):\n        \"\"\"Test multiple concurrent WebSocket connections.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/echo\"\n\n        async def echo_task(task_id):\n            async with await websocket_connect(ws_url) as ws:\n                message = f\"task-{task_id}\"\n                await ws.send(message)\n                response = await ws.recv()\n                assert response == message\n                return task_id\n\n        # Run 10 concurrent connections\n        tasks = [echo_task(i) for i in range(10)]\n        results = await asyncio.gather(*tasks)\n        assert len(results) == 10\n        assert set(results) == set(range(10))\n\n    async def test_concurrent_different_endpoints(self, websocket_connect, gunicorn_url):\n        \"\"\"Test concurrent connections to different endpoints.\"\"\"\n        base_ws = gunicorn_url.replace(\"http://\", \"ws://\")\n\n        async def echo_text():\n            async with await websocket_connect(base_ws + \"/ws/echo\") as ws:\n                await ws.send(\"text\")\n                return await ws.recv()\n\n        async def echo_binary():\n            async with await websocket_connect(base_ws + \"/ws/echo-binary\") as ws:\n                await ws.send(b\"binary\")\n                return await ws.recv()\n\n        async def get_scope():\n            async with await websocket_connect(base_ws + \"/ws/scope\") as ws:\n                return await ws.recv()\n\n        results = await asyncio.gather(\n            echo_text(),\n            echo_binary(),\n            get_scope(),\n        )\n\n        assert results[0] == \"text\"\n        assert results[1] == b\"binary\"\n        scope = json.loads(results[2])\n        assert scope[\"type\"] == \"websocket\"\n\n\n# ============================================================================\n# Edge Cases\n# ============================================================================\n\n@pytest.mark.asyncio\nclass TestWebSocketEdgeCases:\n    \"\"\"Test WebSocket edge cases.\"\"\"\n\n    async def test_unknown_path(self, websocket_connect, gunicorn_url):\n        \"\"\"Test connection to unknown path.\"\"\"\n        websockets = pytest.importorskip(\"websockets\")\n\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/unknown-path\"\n        async with await websocket_connect(ws_url) as ws:\n            response = await ws.recv()\n            data = json.loads(response)\n            assert data[\"error\"] == \"Unknown path\"\n\n            # Connection will be closed\n            try:\n                await ws.recv()\n            except websockets.exceptions.ConnectionClosed:\n                pass\n\n    async def test_special_characters_in_message(self, websocket_connect, gunicorn_url):\n        \"\"\"Test messages with special characters.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/echo\"\n        async with await websocket_connect(ws_url) as ws:\n            special = \"!@#$%^&*()_+-=[]{}|;':\\\",./<>?\\n\\t\\r\"\n            await ws.send(special)\n            response = await ws.recv()\n            assert response == special\n\n    async def test_null_bytes_in_binary(self, websocket_connect, gunicorn_url):\n        \"\"\"Test binary message with null bytes.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/echo-binary\"\n        async with await websocket_connect(ws_url) as ws:\n            data = b\"\\x00\\x00\\x00\"\n            await ws.send(data)\n            response = await ws.recv()\n            assert response == data\n\n    async def test_json_message(self, websocket_connect, gunicorn_url):\n        \"\"\"Test JSON in text message.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/echo\"\n        async with await websocket_connect(ws_url) as ws:\n            payload = json.dumps({\"key\": \"value\", \"number\": 42, \"list\": [1, 2, 3]})\n            await ws.send(payload)\n            response = await ws.recv()\n            assert json.loads(response) == {\"key\": \"value\", \"number\": 42, \"list\": [1, 2, 3]}\n\n    async def test_rapid_close_reconnect(self, websocket_connect, gunicorn_url):\n        \"\"\"Test rapid close and reconnect.\"\"\"\n        ws_url = gunicorn_url.replace(\"http://\", \"ws://\") + \"/ws/echo\"\n\n        for i in range(5):\n            async with await websocket_connect(ws_url) as ws:\n                await ws.send(f\"iteration {i}\")\n                response = await ws.recv()\n                assert response == f\"iteration {i}\"\n"
  },
  {
    "path": "tests/docker/dirty_arbiter/Dockerfile",
    "content": "FROM python:3.12-slim\n\nWORKDIR /app\n\n# Copy gunicorn source\nCOPY . /app/gunicorn-src\n\n# Install gunicorn and test dependencies\n# setproctitle is needed for process title changes (master, dirty-arbiter, etc.)\nRUN pip install --no-cache-dir /app/gunicorn-src pytest requests setproctitle\n\n# Copy test app files\nCOPY tests/docker/dirty_arbiter/app.py /app/\nCOPY tests/docker/dirty_arbiter/gunicorn_conf.py /app/\n\n# Install procps for process inspection\nRUN apt-get update && apt-get install -y procps && rm -rf /var/lib/apt/lists/*\n\n# Default command - run gunicorn\nCMD [\"gunicorn\", \"app:application\", \"-c\", \"gunicorn_conf.py\"]\n"
  },
  {
    "path": "tests/docker/dirty_arbiter/README.md",
    "content": "# Docker-Based Dirty Arbiter Integration Tests\n\nThis directory contains Docker-based integration tests that verify the dirty\narbiter process lifecycle under realistic conditions.\n\n## Overview\n\nThese tests verify:\n\n1. **Parent Death Detection**: Dirty arbiter self-terminates when main arbiter\n   dies unexpectedly (SIGKILL)\n2. **Orphan Cleanup**: Old dirty arbiter processes are cleaned up on restart\n3. **Respawning**: Main arbiter respawns dirty arbiter when it crashes\n4. **Graceful Shutdown**: Both arbiters exit cleanly on SIGTERM\n\n## Prerequisites\n\n- Docker\n- Python 3.10+\n- pytest\n\n## Quick Start\n\n```bash\n# Build the Docker image\ndocker compose build\n\n# Run all tests\npytest test_parent_death.py -v\n\n# Run specific test\npytest test_parent_death.py::TestParentDeath::test_dirty_arbiter_exits_on_parent_sigkill -v\n```\n\n## Manual Verification\n\nYou can manually verify the behavior:\n\n```bash\n# Start the container\ndocker compose up -d\n\n# Check running processes\ndocker exec dirty_arbiter-gunicorn-1 ps aux | grep gunicorn\n\n# SIGKILL the master and watch dirty arbiter exit\nMASTER_PID=$(docker exec dirty_arbiter-gunicorn-1 pgrep -f \"gunicorn: master\")\ndocker exec dirty_arbiter-gunicorn-1 kill -9 $MASTER_PID\n\n# After ~2 seconds, check that all gunicorn processes exited\ndocker exec dirty_arbiter-gunicorn-1 ps aux | grep gunicorn\n\n# View logs\ndocker logs dirty_arbiter-gunicorn-1\n\n# Cleanup\ndocker compose down\n```\n\n## Test Scenarios\n\n### Scenario 1: Parent SIGKILL\n\nTests that the dirty arbiter detects parent death via ppid check:\n\n1. Start gunicorn with dirty workers\n2. SIGKILL the main arbiter (bypasses graceful shutdown)\n3. Verify dirty arbiter detects ppid change within ~2 seconds\n4. Verify no orphan processes remain\n\n### Scenario 2: Orphan Cleanup\n\nTests the `_cleanup_orphaned_dirty_arbiter()` mechanism:\n\n1. Start gunicorn, note dirty arbiter PID\n2. SIGKILL main arbiter (dirty arbiter becomes orphan)\n3. Restart gunicorn\n4. Verify old dirty arbiter was cleaned up\n5. Verify new dirty arbiter spawned\n\n### Scenario 3: Dirty Arbiter Respawn\n\nTests that main arbiter respawns a dead dirty arbiter:\n\n1. Start gunicorn\n2. SIGKILL the dirty arbiter\n3. Wait for respawn (~1-2 seconds)\n4. Verify new dirty arbiter is running\n\n### Scenario 4: Graceful Shutdown\n\nTests clean shutdown via SIGTERM:\n\n1. Start gunicorn with dirty workers\n2. SIGTERM the main arbiter\n3. Verify both arbiters exit cleanly within graceful_timeout\n4. Verify clean exit logs\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `Dockerfile` | Container build configuration |\n| `docker-compose.yml` | Container orchestration |\n| `app.py` | Simple WSGI app with TestDirtyApp |\n| `gunicorn_conf.py` | Gunicorn configuration |\n| `test_parent_death.py` | pytest integration tests |\n| `README.md` | This file |\n\n## Configuration\n\nThe `gunicorn_conf.py` uses:\n- 1 sync worker\n- 1 dirty worker\n- 5 second graceful timeout (for faster tests)\n- Debug logging\n\n## Expected Log Messages\n\nWhen verifying behavior, look for these log messages:\n\n| Message | Meaning |\n|---------|---------|\n| `Parent changed, shutting down dirty arbiter` | ppid detection triggered |\n| `Killing orphaned dirty arbiter` | Orphan cleanup activated |\n| `Spawning dirty arbiter` | New dirty arbiter being created |\n| `Dirty arbiter exiting` | Clean shutdown |\n\n## Troubleshooting\n\n**Tests time out waiting for container**:\n- Check Docker is running\n- Check no port conflicts on 8000\n- Try `docker compose down` and rebuild\n\n**Dirty arbiter doesn't exit after parent death**:\n- Check ppid detection is working (logs should show check)\n- The check runs every 1 second, so allow 2-3 seconds\n\n**Container logs not showing expected messages**:\n- Verify loglevel is set to \"debug\" in gunicorn_conf.py\n- Check `docker logs <container>` for full output\n"
  },
  {
    "path": "tests/docker/dirty_arbiter/app.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nSimple WSGI and Dirty applications for integration testing.\n\"\"\"\n\nfrom gunicorn.dirty.app import DirtyApp\n\n\ndef application(environ, start_response):\n    \"\"\"Simple WSGI application.\"\"\"\n    start_response('200 OK', [('Content-Type', 'text/plain')])\n    return [b'OK']\n\n\nclass TestDirtyApp(DirtyApp):\n    \"\"\"Minimal dirty app for testing process lifecycle.\"\"\"\n\n    def init(self):\n        self.call_count = 0\n\n    def ping(self):\n        self.call_count += 1\n        return {\"pong\": True, \"calls\": self.call_count}\n\n    def echo(self, message):\n        return {\"message\": message}\n"
  },
  {
    "path": "tests/docker/dirty_arbiter/docker-compose.yml",
    "content": "services:\n  gunicorn:\n    build:\n      context: ../../..\n      dockerfile: tests/docker/dirty_arbiter/Dockerfile\n    ports:\n      - \"8000:8000\"\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:8000/\"]\n      interval: 1s\n      timeout: 1s\n      retries: 30\n    stop_grace_period: 10s\n"
  },
  {
    "path": "tests/docker/dirty_arbiter/gunicorn_conf.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nGunicorn configuration for integration tests.\n\"\"\"\n\nbind = \"0.0.0.0:8000\"\nworkers = 1\nworker_class = \"sync\"\ndirty_workers = 1\ndirty_apps = [\"app:TestDirtyApp\"]\ndirty_timeout = 30\ndirty_graceful_timeout = 5\ntimeout = 30\ngraceful_timeout = 5\nloglevel = \"debug\"\naccesslog = \"-\"\nerrorlog = \"-\"\n"
  },
  {
    "path": "tests/docker/dirty_arbiter/test_parent_death.py",
    "content": "#!/usr/bin/env python\n#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nDocker-based integration tests for dirty arbiter process lifecycle.\n\nThese tests verify:\n1. Dirty arbiter self-terminates when main arbiter dies unexpectedly (SIGKILL)\n2. Orphan cleanup works on gunicorn restart\n3. Dirty arbiter respawn works when it dies\n4. Graceful shutdown terminates both arbiters cleanly\n\nUsage:\n    # Build the container first\n    docker compose build\n\n    # Run all tests\n    pytest test_parent_death.py -v\n\n    # Run specific test\n    pytest test_parent_death.py::TestParentDeath::test_dirty_arbiter_exits_on_parent_sigkill -v\n\"\"\"\n\nimport os\nimport re\nimport subprocess\nimport time\n\nimport pytest\n\n\nclass DockerContainer:\n    \"\"\"Context manager for managing a Docker container.\"\"\"\n\n    def __init__(self, name=\"gunicorn-test\", build=True):\n        self.name = name\n        self.build = build\n        self.container_id = None\n\n    def __enter__(self):\n        # Build if requested\n        if self.build:\n            result = subprocess.run(\n                [\"docker\", \"compose\", \"build\"],\n                cwd=os.path.dirname(__file__),\n                capture_output=True,\n                text=True,\n            )\n            if result.returncode != 0:\n                raise RuntimeError(f\"Docker build failed: {result.stderr}\")\n\n        # Remove any existing container with same name\n        subprocess.run(\n            [\"docker\", \"rm\", \"-f\", self.name],\n            capture_output=True,\n        )\n\n        # Start container with a keep-alive wrapper\n        # This runs gunicorn in background so killing master doesn't exit container\n        # The wrapper keeps container alive for observation after master death\n        result = subprocess.run(\n            [\n                \"docker\", \"run\", \"-d\",\n                \"--name\", self.name,\n                \"-p\", \"8000:8000\",\n                \"dirty_arbiter-gunicorn\",\n                \"sh\", \"-c\",\n                \"gunicorn app:application -c gunicorn_conf.py & \"\n                \"GUNICORN_PID=$!; \"\n                \"trap 'kill $GUNICORN_PID 2>/dev/null' TERM; \"\n                \"while true; do sleep 1; done\"\n            ],\n            capture_output=True,\n            text=True,\n        )\n        if result.returncode != 0:\n            raise RuntimeError(f\"Docker run failed: {result.stderr}\")\n\n        self.container_id = result.stdout.strip()\n\n        # Wait for gunicorn to be ready\n        self._wait_for_ready()\n\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        if self.container_id:\n            # Get logs before cleanup\n            logs = self.get_logs()\n            if exc_val:\n                print(f\"\\n=== Container logs ===\\n{logs}\\n=== End logs ===\\n\")\n\n            # Stop and remove container\n            subprocess.run(\n                [\"docker\", \"rm\", \"-f\", self.name],\n                capture_output=True,\n            )\n\n    def _wait_for_ready(self, timeout=30):\n        \"\"\"Wait for gunicorn to be ready.\"\"\"\n        start = time.time()\n        while time.time() - start < timeout:\n            pids = self.get_gunicorn_pids()\n            if pids.get(\"master\") and pids.get(\"dirty-arbiter\"):\n                # Both processes are running\n                return\n            time.sleep(0.5)\n        raise TimeoutError(\"Gunicorn did not start within timeout\")\n\n    def exec(self, cmd, check=True):\n        \"\"\"Execute a command in the container.\"\"\"\n        result = subprocess.run(\n            [\"docker\", \"exec\", self.name] + cmd,\n            capture_output=True,\n            text=True,\n        )\n        if check and result.returncode != 0:\n            raise RuntimeError(f\"Command failed: {cmd}\\n{result.stderr}\")\n        return result\n\n    def get_logs(self):\n        \"\"\"Get container logs.\"\"\"\n        result = subprocess.run(\n            [\"docker\", \"logs\", self.name],\n            capture_output=True,\n            text=True,\n        )\n        return result.stdout + result.stderr\n\n    def get_gunicorn_pids(self):\n        \"\"\"Get PIDs of gunicorn processes.\n\n        Uses ps output with proctitle if available, otherwise falls back\n        to process tree analysis.\n        \"\"\"\n        pids = {\n            \"master\": None,\n            \"dirty-arbiter\": None,\n            \"workers\": [],\n            \"dirty-workers\": [],\n        }\n\n        # First try using proctitle-based detection\n        result = self.exec([\"ps\", \"aux\"], check=False)\n        proctitle_found = False\n\n        for line in result.stdout.split(\"\\n\"):\n            if \"gunicorn:\" not in line:\n                continue\n\n            proctitle_found = True\n            parts = line.split()\n            if len(parts) < 2:\n                continue\n\n            pid = int(parts[1])\n\n            if \"gunicorn: master\" in line:\n                pids[\"master\"] = pid\n            elif \"gunicorn: dirty-arbiter\" in line:\n                pids[\"dirty-arbiter\"] = pid\n            elif \"gunicorn: dirty-worker\" in line:\n                pids[\"dirty-workers\"].append(pid)\n            elif \"gunicorn: worker\" in line:\n                pids[\"workers\"].append(pid)\n\n        if proctitle_found:\n            return pids\n\n        # Fallback: use process tree analysis\n        # Get ps output with ppid info\n        result = self.exec([\"ps\", \"-eo\", \"pid,ppid,comm\"], check=False)\n\n        gunicorn_procs = []\n        for line in result.stdout.split(\"\\n\"):\n            if \"gunicorn\" not in line and \"python\" not in line:\n                continue\n            parts = line.split()\n            if len(parts) >= 3:\n                try:\n                    pid = int(parts[0])\n                    ppid = int(parts[1])\n                    gunicorn_procs.append((pid, ppid))\n                except ValueError:\n                    continue\n\n        # Build process tree\n        # Master: gunicorn process whose parent is init (pid 1 or docker-init)\n        # Dirty-arbiter: child of master\n        # Workers: children of master (that aren't dirty-arbiter)\n        # Dirty-workers: children of dirty-arbiter\n\n        for pid, ppid in gunicorn_procs:\n            if ppid == 1 or ppid == 0:\n                # This is the master (or docker-init spawned process)\n                # Check if it's actually docker-init by checking its children\n                continue\n            if ppid not in [p for p, _ in gunicorn_procs]:\n                # Parent isn't a gunicorn process - this is master\n                pids[\"master\"] = pid\n\n        # Now identify children\n        if pids[\"master\"]:\n            master_children = [p for p, pp in gunicorn_procs if pp == pids[\"master\"]]\n\n            # Get first child as dirty-arbiter (forked first from spawn_dirty_arbiter)\n            # and check if it has children (dirty workers)\n            for child_pid in master_children:\n                child_children = [p for p, pp in gunicorn_procs if pp == child_pid]\n                if child_children:\n                    # This child has children, so it's the dirty-arbiter\n                    pids[\"dirty-arbiter\"] = child_pid\n                    pids[\"dirty-workers\"] = child_children\n                else:\n                    # No children, it's a regular worker\n                    pids[\"workers\"].append(child_pid)\n\n        return pids\n\n    def kill_process(self, pid, signal=9):\n        \"\"\"Send a signal to a process in the container.\"\"\"\n        self.exec(\n            [\"kill\", f\"-{signal}\", str(pid)],\n            check=False,\n        )\n\n    def wait_for_process_exit(self, pid, timeout=5):\n        \"\"\"Wait for a specific process to exit.\"\"\"\n        start = time.time()\n        while time.time() - start < timeout:\n            result = self.exec(\n                [\"ps\", \"-p\", str(pid)],\n                check=False,\n            )\n            if result.returncode != 0:\n                # Process no longer exists\n                return True\n            time.sleep(0.2)\n        return False\n\n    def wait_for_no_gunicorn(self, timeout=5):\n        \"\"\"Wait until no gunicorn processes are running.\"\"\"\n        start = time.time()\n        while time.time() - start < timeout:\n            pids = self.get_gunicorn_pids()\n            if not any([\n                pids[\"master\"],\n                pids[\"dirty-arbiter\"],\n                pids[\"workers\"],\n                pids[\"dirty-workers\"],\n            ]):\n                return True\n            time.sleep(0.2)\n        return False\n\n    def wait_for_dirty_arbiter(self, timeout=10, exclude_pid=None):\n        \"\"\"Wait for a dirty arbiter to be running.\"\"\"\n        start = time.time()\n        while time.time() - start < timeout:\n            pids = self.get_gunicorn_pids()\n            da_pid = pids.get(\"dirty-arbiter\")\n            if da_pid and da_pid != exclude_pid:\n                return da_pid\n            time.sleep(0.5)\n        return None\n\n    def restart_gunicorn(self):\n        \"\"\"Restart gunicorn in the container.\"\"\"\n        # Start gunicorn in background\n        self.exec(\n            [\"sh\", \"-c\", \"gunicorn app:application -c gunicorn_conf.py &\"],\n            check=False,\n        )\n        # Wait for it to be ready\n        self._wait_for_ready()\n\n\nclass TestParentDeath:\n    \"\"\"Test dirty arbiter behavior when parent dies.\"\"\"\n\n    @pytest.fixture(autouse=True)\n    def setup(self):\n        \"\"\"Check Docker is available.\"\"\"\n        result = subprocess.run(\n            [\"docker\", \"info\"],\n            capture_output=True,\n        )\n        if result.returncode != 0:\n            pytest.skip(\"Docker is not available\")\n\n    def test_dirty_arbiter_exits_on_parent_sigkill(self):\n        \"\"\"Dirty arbiter should exit when main arbiter is SIGKILLed.\n\n        This tests the ppid detection mechanism in the dirty arbiter.\n        When the main arbiter is killed with SIGKILL (which bypasses\n        graceful shutdown), the dirty arbiter should detect the parent\n        change and exit within ~2 seconds.\n        \"\"\"\n        with DockerContainer() as container:\n            # Get initial PIDs\n            pids = container.get_gunicorn_pids()\n            master_pid = pids[\"master\"]\n            dirty_arbiter_pid = pids[\"dirty-arbiter\"]\n\n            assert master_pid is not None, \"Master should be running\"\n            assert dirty_arbiter_pid is not None, \"Dirty arbiter should be running\"\n\n            # SIGKILL the main arbiter (bypasses graceful shutdown)\n            container.kill_process(master_pid, signal=9)\n\n            # Wait for dirty arbiter to detect parent death and exit\n            # The ppid check runs every 1 second. During shutdown, the arbiter\n            # may take extra time to complete worker cleanup and handle SIGCHLD.\n            exited = container.wait_for_process_exit(dirty_arbiter_pid, timeout=10)\n\n            assert exited, (\n                f\"Dirty arbiter (pid:{dirty_arbiter_pid}) should have exited \"\n                \"after parent was killed\"\n            )\n\n            # Verify no orphan gunicorn processes remain\n            # HTTP workers check ppid during request loop, so may take longer to exit\n            assert container.wait_for_no_gunicorn(timeout=15), (\n                \"No gunicorn processes should remain after parent death\"\n            )\n\n            # Check logs for expected message\n            logs = container.get_logs()\n            assert \"Parent changed, shutting down dirty arbiter\" in logs, (\n                \"Dirty arbiter should log parent death detection\"\n            )\n\n    def test_orphan_cleanup_on_restart(self):\n        \"\"\"Orphaned dirty arbiter should be cleaned up on restart.\n\n        This tests the _cleanup_orphaned_dirty_arbiter() mechanism.\n        When gunicorn restarts after a crash, it should kill any\n        orphaned dirty arbiter from the previous instance.\n        \"\"\"\n        with DockerContainer() as container:\n            # Get initial PIDs\n            pids = container.get_gunicorn_pids()\n            master_pid = pids[\"master\"]\n            dirty_arbiter_pid = pids[\"dirty-arbiter\"]\n\n            assert master_pid is not None\n            assert dirty_arbiter_pid is not None\n\n            # SIGKILL the main arbiter - dirty arbiter becomes orphan\n            # but will self-terminate via ppid detection\n            container.kill_process(master_pid, signal=9)\n\n            # Wait for all gunicorn processes to exit before restarting\n            # (including HTTP workers which take longer due to ppid check interval)\n            container.wait_for_no_gunicorn(timeout=20)\n\n            # Now restart gunicorn\n            container.restart_gunicorn()\n\n            # Get new PIDs\n            new_pids = container.get_gunicorn_pids()\n            new_dirty_arbiter_pid = new_pids[\"dirty-arbiter\"]\n\n            assert new_dirty_arbiter_pid is not None, (\n                \"New dirty arbiter should have spawned\"\n            )\n            assert new_dirty_arbiter_pid != dirty_arbiter_pid, (\n                \"New dirty arbiter should have different PID\"\n            )\n\n            # Check logs for orphan cleanup or normal startup\n            logs = container.get_logs()\n            # Either the orphan was cleaned up, or ppid detection worked\n            assert (\n                \"Killing orphaned dirty arbiter\" in logs or\n                \"Parent changed, shutting down dirty arbiter\" in logs or\n                \"Dirty arbiter starting\" in logs\n            )\n\n    def test_dirty_arbiter_respawn(self):\n        \"\"\"Main arbiter should respawn dead dirty arbiter.\n\n        When the dirty arbiter dies (e.g., killed or crashed), the main\n        arbiter should detect this and spawn a new one.\n        \"\"\"\n        with DockerContainer() as container:\n            # Get initial PIDs\n            pids = container.get_gunicorn_pids()\n            master_pid = pids[\"master\"]\n            old_dirty_arbiter_pid = pids[\"dirty-arbiter\"]\n\n            assert master_pid is not None\n            assert old_dirty_arbiter_pid is not None\n\n            # SIGKILL the dirty arbiter\n            container.kill_process(old_dirty_arbiter_pid, signal=9)\n\n            # Wait for respawn - main arbiter should spawn a new one\n            new_dirty_arbiter_pid = container.wait_for_dirty_arbiter(\n                timeout=10,\n                exclude_pid=old_dirty_arbiter_pid,\n            )\n\n            assert new_dirty_arbiter_pid is not None, (\n                \"Main arbiter should respawn dirty arbiter\"\n            )\n            assert new_dirty_arbiter_pid != old_dirty_arbiter_pid, (\n                \"New dirty arbiter should have different PID\"\n            )\n\n            # Verify main arbiter is still running\n            pids = container.get_gunicorn_pids()\n            assert pids[\"master\"] == master_pid, (\n                \"Main arbiter should still be running\"\n            )\n\n            # Check logs\n            logs = container.get_logs()\n            assert \"Spawning dirty arbiter\" in logs or \"Spawned dirty arbiter\" in logs\n\n    def test_graceful_shutdown(self):\n        \"\"\"SIGTERM should cleanly shutdown both arbiters.\n\n        When the main arbiter receives SIGTERM, it should signal the\n        dirty arbiter and wait for both to exit cleanly.\n        \"\"\"\n        with DockerContainer() as container:\n            # Get initial PIDs\n            pids = container.get_gunicorn_pids()\n            master_pid = pids[\"master\"]\n            dirty_arbiter_pid = pids[\"dirty-arbiter\"]\n\n            assert master_pid is not None\n            assert dirty_arbiter_pid is not None\n\n            # Send SIGTERM to main arbiter\n            container.kill_process(master_pid, signal=15)\n\n            # Wait for both to exit cleanly\n            # Graceful timeout is 5 seconds in config\n            assert container.wait_for_no_gunicorn(timeout=10), (\n                \"All gunicorn processes should exit on SIGTERM\"\n            )\n\n            # Check logs for graceful shutdown indicators\n            logs = container.get_logs()\n            assert \"Dirty arbiter exiting\" in logs, (\n                \"Dirty arbiter should log clean exit\"\n            )\n\n    def test_sigquit_quick_shutdown(self):\n        \"\"\"SIGQUIT should quickly shutdown both arbiters.\n\n        SIGQUIT triggers a faster shutdown than SIGTERM.\n        \"\"\"\n        with DockerContainer() as container:\n            # Get initial PIDs\n            pids = container.get_gunicorn_pids()\n            master_pid = pids[\"master\"]\n            dirty_arbiter_pid = pids[\"dirty-arbiter\"]\n\n            assert master_pid is not None\n            assert dirty_arbiter_pid is not None\n\n            # Send SIGQUIT to main arbiter\n            container.kill_process(master_pid, signal=3)\n\n            # Both should exit quickly\n            assert container.wait_for_no_gunicorn(timeout=5), (\n                \"All gunicorn processes should exit on SIGQUIT\"\n            )\n\n\nclass TestDirtyArbiterWorkers:\n    \"\"\"Test dirty arbiter worker management.\"\"\"\n\n    @pytest.fixture(autouse=True)\n    def setup(self):\n        \"\"\"Check Docker is available.\"\"\"\n        result = subprocess.run(\n            [\"docker\", \"info\"],\n            capture_output=True,\n        )\n        if result.returncode != 0:\n            pytest.skip(\"Docker is not available\")\n\n    def test_dirty_worker_exists(self):\n        \"\"\"Dirty arbiter should spawn dirty worker(s).\"\"\"\n        with DockerContainer() as container:\n            pids = container.get_gunicorn_pids()\n\n            assert pids[\"master\"] is not None\n            assert pids[\"dirty-arbiter\"] is not None\n            assert len(pids[\"dirty-workers\"]) >= 1, (\n                \"At least one dirty worker should be running\"\n            )\n\n    def test_dirty_worker_respawn(self):\n        \"\"\"Dirty arbiter should respawn killed dirty workers.\"\"\"\n        with DockerContainer() as container:\n            pids = container.get_gunicorn_pids()\n            old_dirty_worker_pid = pids[\"dirty-workers\"][0]\n\n            # Kill the dirty worker\n            container.kill_process(old_dirty_worker_pid, signal=9)\n\n            # Wait for respawn\n            start = time.time()\n            new_dirty_worker_pid = None\n            while time.time() - start < 10:\n                pids = container.get_gunicorn_pids()\n                if pids[\"dirty-workers\"]:\n                    new_pid = pids[\"dirty-workers\"][0]\n                    if new_pid != old_dirty_worker_pid:\n                        new_dirty_worker_pid = new_pid\n                        break\n                time.sleep(0.5)\n\n            assert new_dirty_worker_pid is not None, (\n                \"Dirty arbiter should respawn killed dirty worker\"\n            )\n\n\nif __name__ == \"__main__\":\n    pytest.main([__file__, \"-v\"])\n"
  },
  {
    "path": "tests/docker/dirty_ttin_ttou/Dockerfile",
    "content": "FROM python:3.12-slim\n\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n    curl procps \\\n    && rm -rf /var/lib/apt/lists/*\n\nWORKDIR /app\n\n# Install gunicorn from source\nCOPY . /gunicorn-src/\nRUN pip install --no-cache-dir /gunicorn-src/\n\n# Copy test app\nCOPY tests/docker/dirty_ttin_ttou/app.py /app/\nCOPY tests/docker/dirty_ttin_ttou/gunicorn_conf.py /app/\n\nCMD [\"gunicorn\", \"-c\", \"gunicorn_conf.py\", \"app:app\"]\n"
  },
  {
    "path": "tests/docker/dirty_ttin_ttou/__init__.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Docker integration tests for dirty arbiter TTIN/TTOU signals.\"\"\"\n"
  },
  {
    "path": "tests/docker/dirty_ttin_ttou/app.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Test app with multiple dirty tasks for TTIN/TTOU testing.\"\"\"\n\nimport json\nimport time\n\nfrom gunicorn.dirty import DirtyApp, get_dirty_client\n\n\n# Unlimited workers - runs on all dirty workers\nclass UnlimitedTask(DirtyApp):\n    \"\"\"Task that runs on all dirty workers.\"\"\"\n\n    def setup(self):\n        pass\n\n    def process(self, data):\n        return {\"task\": \"unlimited\", \"data\": data}\n\n\n# Limited to 2 workers\nclass LimitedTask(DirtyApp):\n    \"\"\"Task limited to 2 workers.\"\"\"\n\n    workers = 2\n\n    def setup(self):\n        pass\n\n    def process(self, data):\n        delay = data.get(\"delay\", 0)\n        if delay:\n            time.sleep(delay)\n        return {\"task\": \"limited\", \"data\": data}\n\n\ndef app(environ, start_response):\n    \"\"\"Simple WSGI app for testing.\"\"\"\n    path = environ.get('PATH_INFO', '/')\n\n    if path == '/health':\n        start_response('200 OK', [('Content-Type', 'text/plain')])\n        return [b'OK']\n\n    if path == '/unlimited':\n        try:\n            client = get_dirty_client()\n            result = client.execute('app:UnlimitedTask', {'test': 'data'})\n            start_response('200 OK', [('Content-Type', 'application/json')])\n            return [json.dumps(result).encode()]\n        except Exception as e:\n            start_response('500 Internal Server Error',\n                           [('Content-Type', 'text/plain')])\n            return [str(e).encode()]\n\n    if path == '/limited':\n        try:\n            client = get_dirty_client()\n            result = client.execute('app:LimitedTask', {'test': 'data'})\n            start_response('200 OK', [('Content-Type', 'application/json')])\n            return [json.dumps(result).encode()]\n        except Exception as e:\n            start_response('500 Internal Server Error',\n                           [('Content-Type', 'text/plain')])\n            return [str(e).encode()]\n\n    start_response('404 Not Found', [('Content-Type', 'text/plain')])\n    return [b'Not Found']\n"
  },
  {
    "path": "tests/docker/dirty_ttin_ttou/docker-compose.yml",
    "content": "services:\n  gunicorn:\n    build:\n      context: ../../..\n      dockerfile: tests/docker/dirty_ttin_ttou/Dockerfile\n    ports:\n      - \"18000:8000\"\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:8000/health\"]\n      interval: 2s\n      timeout: 5s\n      retries: 15\n      start_period: 5s\n    stop_grace_period: 10s\n"
  },
  {
    "path": "tests/docker/dirty_ttin_ttou/gunicorn_conf.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Gunicorn configuration for TTIN/TTOU testing.\"\"\"\n\nbind = \"0.0.0.0:8000\"\nworkers = 2\nworker_class = \"gthread\"\nthreads = 2\n\n# Dirty arbiter config\ndirty_apps = [\n    \"app:UnlimitedTask\",\n    \"app:LimitedTask\",  # Has workers=2 attribute\n]\ndirty_workers = 3\ndirty_timeout = 30\n\n# Logging\nloglevel = \"debug\"\naccesslog = \"-\"\nerrorlog = \"-\"\n"
  },
  {
    "path": "tests/docker/dirty_ttin_ttou/test_ttin_ttou_docker.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Docker integration tests for dirty arbiter TTIN/TTOU signals.\"\"\"\n\nimport os\nimport subprocess\nimport time\nfrom pathlib import Path\n\nimport pytest\nimport requests\n\n\npytestmark = [\n    pytest.mark.docker,\n    pytest.mark.integration,\n]\n\n# Directory containing this test file\nTEST_DIR = Path(__file__).parent\nCOMPOSE_FILE = TEST_DIR / \"docker-compose.yml\"\nBASE_URL = \"http://localhost:18000\"\n\n\n@pytest.fixture(scope=\"module\")\ndef docker_services():\n    \"\"\"Start Docker services for the test module.\"\"\"\n    # Start services\n    subprocess.run(\n        [\"docker\", \"compose\", \"-f\", str(COMPOSE_FILE), \"up\", \"-d\", \"--build\"],\n        check=True,\n        cwd=TEST_DIR\n    )\n\n    # Wait for health\n    for _ in range(30):\n        try:\n            resp = requests.get(f\"{BASE_URL}/health\", timeout=2)\n            if resp.status_code == 200:\n                break\n        except requests.RequestException:\n            pass\n        time.sleep(1)\n    else:\n        # Print logs for debugging\n        subprocess.run(\n            [\"docker\", \"compose\", \"-f\", str(COMPOSE_FILE), \"logs\"],\n            cwd=TEST_DIR\n        )\n        pytest.fail(\"Services did not become healthy\")\n\n    yield\n\n    # Cleanup\n    subprocess.run(\n        [\"docker\", \"compose\", \"-f\", str(COMPOSE_FILE), \"down\", \"-v\"],\n        cwd=TEST_DIR\n    )\n\n\ndef get_dirty_arbiter_pid():\n    \"\"\"Get the dirty arbiter PID from the container.\"\"\"\n    result = subprocess.run(\n        [\"docker\", \"compose\", \"-f\", str(COMPOSE_FILE),\n         \"exec\", \"-T\", \"gunicorn\", \"pgrep\", \"-f\", \"dirty-arbiter\"],\n        capture_output=True,\n        text=True,\n        cwd=TEST_DIR\n    )\n    pids = result.stdout.strip().split('\\n')\n    # Return the first PID (there should only be one dirty-arbiter)\n    return int(pids[0]) if pids and pids[0] else None\n\n\ndef get_dirty_worker_count():\n    \"\"\"Get the current number of dirty workers.\"\"\"\n    result = subprocess.run(\n        [\"docker\", \"compose\", \"-f\", str(COMPOSE_FILE),\n         \"exec\", \"-T\", \"gunicorn\", \"pgrep\", \"-c\", \"-f\", \"dirty-worker\"],\n        capture_output=True,\n        text=True,\n        cwd=TEST_DIR\n    )\n    count = result.stdout.strip()\n    return int(count) if count else 0\n\n\ndef send_signal_to_dirty_arbiter(sig):\n    \"\"\"Send a signal to the dirty arbiter.\"\"\"\n    pid = get_dirty_arbiter_pid()\n    if pid is None:\n        raise RuntimeError(\"Could not find dirty arbiter PID\")\n    subprocess.run(\n        [\"docker\", \"compose\", \"-f\", str(COMPOSE_FILE),\n         \"exec\", \"-T\", \"gunicorn\", \"kill\", f\"-{sig}\", str(pid)],\n        check=True,\n        cwd=TEST_DIR\n    )\n\n\nclass TestTTINSignal:\n    \"\"\"Test SIGTTIN increases dirty workers.\"\"\"\n\n    def test_ttin_increases_workers(self, docker_services):\n        \"\"\"TTIN should spawn additional dirty worker.\"\"\"\n        initial_count = get_dirty_worker_count()\n        assert initial_count == 3, f\"Expected 3 initial workers, got {initial_count}\"\n\n        send_signal_to_dirty_arbiter(\"TTIN\")\n        time.sleep(2)  # Wait for worker to spawn\n\n        new_count = get_dirty_worker_count()\n        assert new_count == 4, f\"Expected 4 workers after TTIN, got {new_count}\"\n\n    def test_multiple_ttin_increases(self, docker_services):\n        \"\"\"Multiple TTIN signals should keep increasing workers.\"\"\"\n        # Get current count (may be 4 from previous test)\n        current_count = get_dirty_worker_count()\n\n        send_signal_to_dirty_arbiter(\"TTIN\")\n        time.sleep(2)\n\n        new_count = get_dirty_worker_count()\n        assert new_count == current_count + 1\n\n\nclass TestTTOUSignal:\n    \"\"\"Test SIGTTOU decreases dirty workers.\"\"\"\n\n    def test_ttou_decreases_workers(self, docker_services):\n        \"\"\"TTOU should kill a dirty worker.\"\"\"\n        # First make sure we have more than minimum\n        send_signal_to_dirty_arbiter(\"TTIN\")\n        time.sleep(2)\n\n        count_before = get_dirty_worker_count()\n        send_signal_to_dirty_arbiter(\"TTOU\")\n        time.sleep(2)\n\n        count_after = get_dirty_worker_count()\n        assert count_after == count_before - 1\n\n    def test_ttou_respects_minimum(self, docker_services):\n        \"\"\"TTOU should not go below app minimum (2 for LimitedTask).\"\"\"\n        # Try to decrease multiple times\n        for _ in range(10):\n            send_signal_to_dirty_arbiter(\"TTOU\")\n            time.sleep(0.5)\n\n        time.sleep(2)  # Wait for all signals to be processed\n\n        # Should not go below 2 (LimitedTask.workers = 2)\n        final_count = get_dirty_worker_count()\n        assert final_count >= 2, f\"Worker count {final_count} is below minimum of 2\"\n\n\nclass TestUnlimitedApps:\n    \"\"\"Test apps with worker_count=None work correctly.\"\"\"\n\n    def test_unlimited_app_works(self, docker_services):\n        \"\"\"UnlimitedTask should work.\"\"\"\n        resp = requests.get(f\"{BASE_URL}/unlimited\", timeout=10)\n        assert resp.status_code == 200\n        data = resp.json()\n        assert data[\"task\"] == \"unlimited\"\n\n    def test_limited_app_works(self, docker_services):\n        \"\"\"LimitedTask should work.\"\"\"\n        resp = requests.get(f\"{BASE_URL}/limited\", timeout=10)\n        assert resp.status_code == 200\n        data = resp.json()\n        assert data[\"task\"] == \"limited\"\n\n    def test_apps_work_after_scaling(self, docker_services):\n        \"\"\"Both apps should work after scaling up and down.\"\"\"\n        # Scale up\n        send_signal_to_dirty_arbiter(\"TTIN\")\n        time.sleep(2)\n\n        # Test both apps\n        resp = requests.get(f\"{BASE_URL}/unlimited\", timeout=10)\n        assert resp.status_code == 200\n\n        resp = requests.get(f\"{BASE_URL}/limited\", timeout=10)\n        assert resp.status_code == 200\n\n        # Scale down\n        send_signal_to_dirty_arbiter(\"TTOU\")\n        time.sleep(2)\n\n        # Test both apps again\n        resp = requests.get(f\"{BASE_URL}/unlimited\", timeout=10)\n        assert resp.status_code == 200\n\n        resp = requests.get(f\"{BASE_URL}/limited\", timeout=10)\n        assert resp.status_code == 200\n"
  },
  {
    "path": "tests/docker/http2/Dockerfile.gunicorn",
    "content": "FROM python:3.12-slim\n\n# Install build dependencies for h2 and other packages\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n    gcc \\\n    && rm -rf /var/lib/apt/lists/*\n\nWORKDIR /app\n\n# Copy the gunicorn source code and install it\nCOPY . /gunicorn-src/\nRUN pip install --no-cache-dir /gunicorn-src/[http2]\n\n# Copy the test application\nCOPY tests/docker/http2/app.py /app/app.py\n\nEXPOSE 8443\n\nCMD [\"gunicorn\", \"app:app\", \\\n     \"--bind\", \"0.0.0.0:8443\", \\\n     \"--worker-class\", \"gthread\", \\\n     \"--threads\", \"4\", \\\n     \"--http-protocols\", \"h2,h1\", \\\n     \"--certfile\", \"/certs/server.crt\", \\\n     \"--keyfile\", \"/certs/server.key\", \\\n     \"--workers\", \"2\", \\\n     \"--log-level\", \"debug\"]\n"
  },
  {
    "path": "tests/docker/http2/Dockerfile.nginx",
    "content": "FROM nginx:1.29-alpine\n\n# Install curl for healthcheck\nRUN apk add --no-cache curl\n\n# Copy nginx configuration\nCOPY nginx.conf /etc/nginx/nginx.conf\n\nEXPOSE 8444\n\nCMD [\"nginx\", \"-g\", \"daemon off;\"]\n"
  },
  {
    "path": "tests/docker/http2/README.rst",
    "content": "HTTP/2 Docker Integration Tests\n================================\n\nThis directory contains Docker-based integration tests for HTTP/2 support\nin Gunicorn. These tests verify real HTTP/2 connections using actual HTTP/2\nclients, both directly to Gunicorn and through an nginx reverse proxy.\n\nPrerequisites\n-------------\n\n- Docker and Docker Compose\n- OpenSSL (for generating test certificates)\n- Python with ``httpx[http2]`` installed\n\nRunning the Tests\n-----------------\n\n1. Install test dependencies::\n\n    pip install -e \".[testing]\"\n\n2. Generate SSL certificates (done automatically by tests, or manually)::\n\n    cd tests/docker/http2\n    openssl req -x509 -newkey rsa:2048 \\\n        -keyout certs/server.key \\\n        -out certs/server.crt \\\n        -days 1 -nodes \\\n        -subj \"/CN=localhost\"\n\n3. Run the Docker integration tests::\n\n    # From the project root\n    pytest tests/docker/http2/ -v\n\n   Or with Docker Compose manually::\n\n    cd tests/docker/http2\n    docker compose up -d\n    pytest -v\n    docker compose down -v\n\nTest Categories\n---------------\n\n- **TestDirectHTTP2Connection**: Direct HTTP/2 connections to Gunicorn\n- **TestConcurrentStreams**: HTTP/2 multiplexing with concurrent streams\n- **TestHTTP2BehindProxy**: HTTP/2 through nginx reverse proxy\n- **TestHTTP2Protocol**: ALPN negotiation and protocol fallback\n- **TestHTTP2ErrorHandling**: Error responses over HTTP/2\n- **TestHTTP2Headers**: HTTP/2 header handling\n- **TestHTTP2Performance**: Performance-related tests\n\nArchitecture\n------------\n\n::\n\n    +--------+     HTTP/2      +-----------+\n    | Client | --------------> | Gunicorn  |\n    +--------+                 | (port 8443)|\n         |                     +-----------+\n         |\n         |     HTTP/2      +-------+    HTTPS     +-----------+\n         +---------------> | nginx | -----------> | Gunicorn  |\n                           | proxy |              | (port 8443)|\n                           | (8444)|              +-----------+\n                           +-------+\n\nFiles\n-----\n\n- ``docker-compose.yml`` - Service definitions\n- ``Dockerfile.gunicorn`` - Gunicorn container with HTTP/2\n- ``Dockerfile.nginx`` - nginx HTTP/2 proxy\n- ``nginx.conf`` - nginx configuration\n- ``app.py`` - Test WSGI application\n- ``conftest.py`` - Pytest fixtures for Docker\n- ``test_http2_docker.py`` - Integration tests\n\nTroubleshooting\n---------------\n\nIf tests fail to start:\n\n1. Check Docker is running::\n\n    docker info\n\n2. Check service logs::\n\n    cd tests/docker/http2\n    docker compose logs gunicorn-h2\n    docker compose logs nginx-h2\n\n3. Verify certificates::\n\n    openssl x509 -in certs/server.crt -text -noout\n\n4. Test manually with curl::\n\n    curl -k --http2 https://localhost:8443/\n    curl -k --http2 https://localhost:8444/\n"
  },
  {
    "path": "tests/docker/http2/__init__.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"HTTP/2 Docker integration tests package.\"\"\"\n"
  },
  {
    "path": "tests/docker/http2/app.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Test WSGI application for HTTP/2 Docker integration tests.\"\"\"\n\nimport json\n\n\ndef app(environ, start_response):\n    \"\"\"Simple WSGI app for testing HTTP/2 functionality.\"\"\"\n    path = environ.get('PATH_INFO', '/')\n    method = environ.get('REQUEST_METHOD', 'GET')\n\n    if path == '/':\n        body = b'Hello HTTP/2!'\n        status = '200 OK'\n        content_type = 'text/plain'\n\n    elif path == '/health':\n        body = b'OK'\n        status = '200 OK'\n        content_type = 'text/plain'\n\n    elif path == '/echo':\n        # Echo back the request body\n        content_length = int(environ.get('CONTENT_LENGTH', 0) or 0)\n        body = environ['wsgi.input'].read(content_length)\n        status = '200 OK'\n        content_type = 'application/octet-stream'\n\n    elif path == '/headers':\n        # Return all HTTP headers as JSON\n        headers = {}\n        for key, value in environ.items():\n            if key.startswith('HTTP_'):\n                headers[key] = value\n        # Also include some important non-HTTP_ headers\n        for key in ['CONTENT_TYPE', 'CONTENT_LENGTH', 'REQUEST_METHOD',\n                    'PATH_INFO', 'QUERY_STRING', 'SERVER_PROTOCOL']:\n            if key in environ:\n                headers[key] = str(environ[key])\n        body = json.dumps(headers, indent=2).encode('utf-8')\n        status = '200 OK'\n        content_type = 'application/json'\n\n    elif path == '/version':\n        # Return HTTP version info\n        server_protocol = environ.get('SERVER_PROTOCOL', 'HTTP/1.1')\n        body = server_protocol.encode('utf-8')\n        status = '200 OK'\n        content_type = 'text/plain'\n\n    elif path == '/large':\n        # Return a large response (1MB) for testing streaming\n        body = b'X' * (1024 * 1024)\n        status = '200 OK'\n        content_type = 'application/octet-stream'\n\n    elif path == '/stream':\n        # Return a streaming response\n        def generate():\n            for i in range(10):\n                yield f'chunk-{i}\\n'.encode('utf-8')\n\n        start_response('200 OK', [\n            ('Content-Type', 'text/plain'),\n            ('Transfer-Encoding', 'chunked')\n        ])\n        return generate()\n\n    elif path == '/status':\n        # Return a specific status code based on query string\n        query = environ.get('QUERY_STRING', '')\n        try:\n            code = int(query.split('=')[1]) if '=' in query else 200\n        except (ValueError, IndexError):\n            code = 200\n        status_messages = {\n            200: 'OK',\n            201: 'Created',\n            204: 'No Content',\n            400: 'Bad Request',\n            404: 'Not Found',\n            500: 'Internal Server Error',\n        }\n        status = f'{code} {status_messages.get(code, \"Unknown\")}'\n        body = f'Status: {code}'.encode('utf-8')\n        content_type = 'text/plain'\n\n    elif path == '/delay':\n        # Simulate a slow response\n        import time\n        query = environ.get('QUERY_STRING', '')\n        try:\n            delay = float(query.split('=')[1]) if '=' in query else 1.0\n            delay = min(delay, 5.0)  # Cap at 5 seconds\n        except (ValueError, IndexError):\n            delay = 1.0\n        time.sleep(delay)\n        body = f'Delayed {delay}s'.encode('utf-8')\n        status = '200 OK'\n        content_type = 'text/plain'\n\n    elif path == '/method':\n        # Return the request method\n        body = method.encode('utf-8')\n        status = '200 OK'\n        content_type = 'text/plain'\n\n    elif path == '/early-hints':\n        # Test endpoint for 103 Early Hints\n        # Send early hints if the callback is available\n        if 'wsgi.early_hints' in environ:\n            environ['wsgi.early_hints']([\n                ('Link', '</style.css>; rel=preload; as=style'),\n                ('Link', '</app.js>; rel=preload; as=script'),\n            ])\n        body = b'Early hints sent!'\n        status = '200 OK'\n        content_type = 'text/plain'\n\n    elif path == '/early-hints-multiple':\n        # Test endpoint for multiple 103 Early Hints responses\n        if 'wsgi.early_hints' in environ:\n            # First early hints\n            environ['wsgi.early_hints']([\n                ('Link', '</critical.css>; rel=preload; as=style'),\n            ])\n            # Second early hints\n            environ['wsgi.early_hints']([\n                ('Link', '</deferred.js>; rel=preload; as=script'),\n            ])\n        body = b'Multiple early hints sent!'\n        status = '200 OK'\n        content_type = 'text/plain'\n\n    else:\n        body = b'Not Found'\n        status = '404 Not Found'\n        content_type = 'text/plain'\n\n    response_headers = [\n        ('Content-Type', content_type),\n        ('Content-Length', str(len(body))),\n        ('X-Request-Path', path),\n        ('X-Request-Method', method),\n    ]\n\n    start_response(status, response_headers)\n    return [body]\n\n\n# For running directly with python\nif __name__ == '__main__':\n    from wsgiref.simple_server import make_server\n    server = make_server('localhost', 8000, app)\n    print('Serving on http://localhost:8000')\n    server.serve_forever()\n"
  },
  {
    "path": "tests/docker/http2/certs/.gitkeep",
    "content": "# This directory contains SSL certificates generated for testing.\n# Certificates are generated automatically by conftest.py.\n# Do not commit actual certificate files.\n"
  },
  {
    "path": "tests/docker/http2/certs/server.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDfDCCAmSgAwIBAgIUDxTarKRHe0FIyczGmoYwm377ZpcwDQYJKoZIhvcNAQEL\nBQAwOTESMBAGA1UEAwwJbG9jYWxob3N0MRYwFAYDVQQKDA1HdW5pY29ybiBUZXN0\nMQswCQYDVQQGEwJVUzAeFw0yNjAyMDUxMTE1MjJaFw0yNjAyMDYxMTE1MjJaMDkx\nEjAQBgNVBAMMCWxvY2FsaG9zdDEWMBQGA1UECgwNR3VuaWNvcm4gVGVzdDELMAkG\nA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCRQTHakkqY\n6l6dMqfs4oiY98+rjvZubwjp0PH7UBuxXCi/4Ao78o0JhKcs+jgAGAXyb8eRjEKt\nz4rPoHZYE91D/eD0lWAz9r/LRoutDJd9IO0rfDtHlYXamciuxJJ8cckOrnuTXLtq\nAWqjKR3U9RIDD3eumCKG4l7Py0L67zTomwMRPfeIdlWBfxGjWMqOewdTc/O/cuK2\nHL5JP2ixy+iTufs0jhljI9cbu49J606f+TQH9eXRTD716q+KsHPJX1X5dVd7V7Lr\nFIp7wSUFdbiy56JfmrGmfJbZgFH67P0ZyiTpQBaVHt1YYRIcOUJZqM+0MAtrsySC\nTNA/LsI8tsybAgMBAAGjfDB6MB0GA1UdDgQWBBRK2VkAeM0hL4j/45ckkKbGrb/Q\nFjAfBgNVHSMEGDAWgBRK2VkAeM0hL4j/45ckkKbGrb/QFjAPBgNVHRMBAf8EBTAD\nAQH/MCcGA1UdEQQgMB6CCWxvY2FsaG9zdIILZ3VuaWNvcm4taDKHBH8AAAEwDQYJ\nKoZIhvcNAQELBQADggEBAAXwuw0KTQUC4UEFudQ1rceK6By9WCSJND7xJi+UQ50G\nZrp5tJ2YB4ZWY+APadfuJo+zUxYVZ3jhs0mxgVeiGdDW6yZdHkeX8MlXBTLHR+/a\nA7DXn6wCw9NDeDtcY/bKg5iamvoGGTL6szPrqeuZPz4UdbsFlr0MdcjgSNOqnkjr\nYS4ukgZ71aWSjfraRRPjFMzkfnQ1xm96A1ngMH4DvU/t62D7r8+SvxQ8M6ERL84Z\nFBu4bTXDdYIjJ24ojmDDO2irTVW1FMGXQTPzMaTEbE1rvBYeEYhf10KiMynK9xfO\n5j8LWmCkgek0CqBrf3zbDEwu8QxcaxITAIUkSXLOZbo=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/docker/http2/certs/server.key",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCRQTHakkqY6l6d\nMqfs4oiY98+rjvZubwjp0PH7UBuxXCi/4Ao78o0JhKcs+jgAGAXyb8eRjEKtz4rP\noHZYE91D/eD0lWAz9r/LRoutDJd9IO0rfDtHlYXamciuxJJ8cckOrnuTXLtqAWqj\nKR3U9RIDD3eumCKG4l7Py0L67zTomwMRPfeIdlWBfxGjWMqOewdTc/O/cuK2HL5J\nP2ixy+iTufs0jhljI9cbu49J606f+TQH9eXRTD716q+KsHPJX1X5dVd7V7LrFIp7\nwSUFdbiy56JfmrGmfJbZgFH67P0ZyiTpQBaVHt1YYRIcOUJZqM+0MAtrsySCTNA/\nLsI8tsybAgMBAAECggEANBhGOYZLI9G2sjlXOaG7bOU/wV9KKaw/7Z/HEaOW8wLD\nCKHg+cQRai79yCdLi1kSVPNbB2vfBDhRqAp8NzWUn0x/8ChcsvZVriF0edFwyWtU\nNErfddp+Absy2t9cTC6A9feFEYJqIug0JyVZciWc2qUi/ubIR0kLyQm00YuWFa/s\nGJou8Nhg70rqW+3FB1H8kAEXqob+PFW4xbTwexw1+MbHxN7UKLTzS8uzYGLo2UpB\n7bksumyD0o+lZtlx9HZ6CwrB6IPjgJ0HyaD8SrOc7/ozd7rR2LmvMmBCV1uC5VSO\njhr0PScLoNv60fjkVOiF9uqaPY2kNKymsOzpZ7/mwQKBgQDMcz+ve8WGGbE+bbM7\n2uinQ5smm8rWPnfbHJIHQUetrEQKljRovybmjiiXN08uxlX6VA/Vnp4fmL5fzsTD\nxTeiCVPsR1huXIfMLGJ6crUgvlbiaB8XsxtVNBpfEEtBe27qjSIj3xtmwqM6+LD1\nFKLsYzgotHUH9JwyLA1RMKPBwQKBgQC14QWtI5YtZcTX46BqxlZ07iAAuy19Jywn\nUtgmTawkJuEcseewIjxtJkMz+aSy7V3PsLII8tY48oSjAVx84w50zLJ2OlJnFT1S\nzEmIOu9YDcGLZkYXJ2AwndRAIXpJVHwtFM9eDSMh+wVPBFeboYP1dO/VxmN6QV0W\nGqDaQfItWwKBgEb31mp2n0j+UB0ofSfQxCOTfx62w4D87CPd1f64tUXe3zuBii21\n9K3hOMvMwiqtZBjh5yEyzxaOsb6WCo0eP0J61GvXFCYy7lx8J67zdFYqXAR5OhnC\n7UD1NhY7lLPlQcofNXOYNW3FMF3/B4X7JNbDVjIi+eDKExIDYpgFN0LBAoGADGCf\n7kR5t+UxHDAVfq64u4RpESOr2NSNoK92nkSy7lLnBvjkd4wc6KCt+h+HIdYdiEDS\nHOHJyl5WwHEbRjR9i11S19DoQrOjVLsqVecM2sU04rO3GWRIm4ZiJ2sf01W4jajY\n4+Go/msC1XnKLIE1ZcLrf3Tc2DkSiKqPP8s1G/kCgYA8sCPAXedwhULhOBM45x4J\nvkwT1Icm5RHOwOr8t34IFozTLokba6pjhYua3nE+V3FglRct7NpX+Op4gUgHa80g\n5zoHboq5/pTUTclx41jndC1YGa3NLvthDWTWmyo/Qj7F/R7jGJf8E3KUDe0tFoSp\nJlfEuUHtKpFJReBnmWTFiQ==\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "tests/docker/http2/conftest.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Pytest fixtures for HTTP/2 Docker integration tests.\"\"\"\n\nimport subprocess\nimport time\nfrom pathlib import Path\n\nimport pytest\n\n# Directory containing this conftest.py\nDOCKER_DIR = Path(__file__).parent\nCERTS_DIR = DOCKER_DIR / \"certs\"\n\n\ndef generate_self_signed_cert(certs_dir: Path) -> None:\n    \"\"\"Generate self-signed SSL certificates for testing.\"\"\"\n    certs_dir.mkdir(parents=True, exist_ok=True)\n    cert_file = certs_dir / \"server.crt\"\n    key_file = certs_dir / \"server.key\"\n\n    # Skip if certs already exist and are recent (less than 1 day old)\n    if cert_file.exists() and key_file.exists():\n        age = time.time() - cert_file.stat().st_mtime\n        if age < 86400:  # 1 day\n            return\n\n    # Generate self-signed certificate\n    subprocess.run(\n        [\n            \"openssl\", \"req\", \"-x509\", \"-newkey\", \"rsa:2048\",\n            \"-keyout\", str(key_file),\n            \"-out\", str(cert_file),\n            \"-days\", \"1\",\n            \"-nodes\",\n            \"-subj\", \"/CN=localhost/O=Gunicorn Test/C=US\",\n            \"-addext\", \"subjectAltName=DNS:localhost,DNS:gunicorn-h2,IP:127.0.0.1\"\n        ],\n        check=True,\n        capture_output=True\n    )\n    # Set readable permissions\n    cert_file.chmod(0o644)\n    key_file.chmod(0o644)\n\n\ndef wait_for_service(url: str, timeout: int = 60) -> bool:\n    \"\"\"Wait for a service to become available.\"\"\"\n    import ssl\n    import socket\n    from urllib.parse import urlparse\n\n    parsed = urlparse(url)\n    host = parsed.hostname or 'localhost'\n    port = parsed.port or 443\n\n    start_time = time.time()\n    while time.time() - start_time < timeout:\n        try:\n            ctx = ssl.create_default_context()\n            ctx.check_hostname = False\n            ctx.verify_mode = ssl.CERT_NONE\n\n            with socket.create_connection((host, port), timeout=5) as sock:\n                with ctx.wrap_socket(sock, server_hostname=host):\n                    return True\n        except (socket.error, ssl.SSLError, OSError):\n            time.sleep(1)\n    return False\n\n\n@pytest.fixture(scope=\"session\")\ndef docker_compose_file():\n    \"\"\"Return the path to docker-compose.yml.\"\"\"\n    return DOCKER_DIR / \"docker-compose.yml\"\n\n\n@pytest.fixture(scope=\"session\")\ndef certs_dir():\n    \"\"\"Generate and return the certs directory.\"\"\"\n    generate_self_signed_cert(CERTS_DIR)\n    return CERTS_DIR\n\n\n@pytest.fixture(scope=\"session\")\ndef docker_services(docker_compose_file, certs_dir):\n    \"\"\"Start Docker services for the test session.\"\"\"\n    compose_file = str(docker_compose_file)\n\n    # Check if Docker is available\n    try:\n        subprocess.run(\n            [\"docker\", \"info\"],\n            check=True,\n            capture_output=True\n        )\n    except (subprocess.CalledProcessError, FileNotFoundError):\n        pytest.skip(\"Docker is not available\")\n\n    # Check if docker compose is available\n    try:\n        subprocess.run(\n            [\"docker\", \"compose\", \"version\"],\n            check=True,\n            capture_output=True\n        )\n    except subprocess.CalledProcessError:\n        pytest.skip(\"Docker Compose is not available\")\n\n    # Build and start services\n    try:\n        subprocess.run(\n            [\"docker\", \"compose\", \"-f\", compose_file, \"build\"],\n            check=True,\n            cwd=DOCKER_DIR\n        )\n        subprocess.run(\n            [\"docker\", \"compose\", \"-f\", compose_file, \"up\", \"-d\"],\n            check=True,\n            cwd=DOCKER_DIR\n        )\n\n        # Wait for services to be healthy\n        gunicorn_ready = wait_for_service(\"https://127.0.0.1:8443\", timeout=60)\n        nginx_ready = wait_for_service(\"https://127.0.0.1:8444\", timeout=60)\n\n        if not gunicorn_ready:\n            # Get logs for debugging\n            result = subprocess.run(\n                [\"docker\", \"compose\", \"-f\", compose_file, \"logs\", \"gunicorn-h2\"],\n                capture_output=True,\n                text=True,\n                cwd=DOCKER_DIR\n            )\n            pytest.fail(f\"Gunicorn service failed to start. Logs:\\n{result.stdout}\\n{result.stderr}\")\n\n        if not nginx_ready:\n            result = subprocess.run(\n                [\"docker\", \"compose\", \"-f\", compose_file, \"logs\", \"nginx-h2\"],\n                capture_output=True,\n                text=True,\n                cwd=DOCKER_DIR\n            )\n            pytest.fail(f\"Nginx service failed to start. Logs:\\n{result.stdout}\\n{result.stderr}\")\n\n        yield {\n            \"gunicorn\": \"https://127.0.0.1:8443\",\n            \"nginx\": \"https://127.0.0.1:8444\"\n        }\n\n    finally:\n        # Stop and remove services\n        subprocess.run(\n            [\"docker\", \"compose\", \"-f\", compose_file, \"down\", \"-v\", \"--remove-orphans\"],\n            cwd=DOCKER_DIR,\n            capture_output=True\n        )\n\n\n@pytest.fixture\ndef gunicorn_url(docker_services):\n    \"\"\"Return the gunicorn service URL.\"\"\"\n    return docker_services[\"gunicorn\"]\n\n\n@pytest.fixture\ndef nginx_url(docker_services):\n    \"\"\"Return the nginx proxy URL.\"\"\"\n    return docker_services[\"nginx\"]\n\n\n@pytest.fixture\ndef h2_client():\n    \"\"\"Create an HTTP/2 capable client.\"\"\"\n    httpx = pytest.importorskip(\"httpx\")\n    client = httpx.Client(http2=True, verify=False, timeout=30.0)\n    yield client\n    client.close()\n\n\n@pytest.fixture\ndef h1_client():\n    \"\"\"Create an HTTP/1.1 only client.\"\"\"\n    httpx = pytest.importorskip(\"httpx\")\n    client = httpx.Client(http2=False, verify=False, timeout=30.0)\n    yield client\n    client.close()\n\n\n@pytest.fixture\ndef async_h2_client():\n    \"\"\"Create an async HTTP/2 capable client.\"\"\"\n    httpx = pytest.importorskip(\"httpx\")\n\n    async def create_client():\n        return httpx.AsyncClient(http2=True, verify=False, timeout=30.0)\n\n    return create_client\n"
  },
  {
    "path": "tests/docker/http2/docker-compose.yml",
    "content": "services:\n  gunicorn-h2:\n    build:\n      context: ../../../\n      dockerfile: tests/docker/http2/Dockerfile.gunicorn\n    ports:\n      - \"8443:8443\"\n    volumes:\n      - ./certs:/certs:ro\n      - ./app.py:/app/app.py:ro\n    environment:\n      - GUNICORN_CERTFILE=/certs/server.crt\n      - GUNICORN_KEYFILE=/certs/server.key\n    healthcheck:\n      test: [\"CMD\", \"python\", \"-c\", \"import ssl,socket; s=socket.socket(); s.settimeout(1); ctx=ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE; ss=ctx.wrap_socket(s,server_hostname='localhost'); ss.connect(('localhost',8443)); ss.close()\"]\n      interval: 2s\n      timeout: 5s\n      retries: 15\n      start_period: 5s\n\n  nginx-h2:\n    build:\n      context: .\n      dockerfile: Dockerfile.nginx\n    ports:\n      - \"8444:8444\"\n    volumes:\n      - ./certs:/certs:ro\n      - ./nginx.conf:/etc/nginx/nginx.conf:ro\n    depends_on:\n      gunicorn-h2:\n        condition: service_healthy\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-k\", \"-f\", \"https://localhost:8444/health\"]\n      interval: 2s\n      timeout: 5s\n      retries: 15\n      start_period: 5s\n\nnetworks:\n  default:\n    driver: bridge\n"
  },
  {
    "path": "tests/docker/http2/nginx.conf",
    "content": "worker_processes auto;\nerror_log /var/log/nginx/error.log warn;\npid /var/run/nginx.pid;\n\nevents {\n    worker_connections 1024;\n}\n\nhttp {\n    include /etc/nginx/mime.types;\n    default_type application/octet-stream;\n\n    log_format main '$remote_addr - $remote_user [$time_local] \"$request\" '\n                    '$status $body_bytes_sent \"$http_referer\" '\n                    '\"$http_user_agent\" \"$http_x_forwarded_for\"';\n\n    access_log /var/log/nginx/access.log main;\n\n    sendfile on;\n    keepalive_timeout 65;\n\n    upstream gunicorn_h2 {\n        server gunicorn-h2:8443;\n        keepalive 32;\n    }\n\n    server {\n        listen 8444 ssl;\n        http2 on;\n        server_name localhost;\n\n        ssl_certificate /certs/server.crt;\n        ssl_certificate_key /certs/server.key;\n        ssl_protocols TLSv1.2 TLSv1.3;\n        ssl_ciphers HIGH:!aNULL:!MD5;\n        ssl_prefer_server_ciphers on;\n\n        # HTTP/2 settings\n        http2_max_concurrent_streams 128;\n\n        # Health check endpoint\n        location /health {\n            return 200 'OK';\n            add_header Content-Type text/plain;\n        }\n\n        location / {\n            # Proxy to gunicorn with HTTPS\n            proxy_pass https://gunicorn_h2;\n            proxy_http_version 1.1;\n            proxy_ssl_verify off;\n            proxy_ssl_server_name on;\n\n            # Enable forwarding of 103 Early Hints from upstream\n            # $http2 is set to \"h2\" when HTTP/2 is used, empty otherwise\n            early_hints $http2;\n\n            # Headers\n            proxy_set_header Host $host;\n            proxy_set_header X-Real-IP $remote_addr;\n            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n            proxy_set_header X-Forwarded-Proto $scheme;\n            proxy_set_header X-Forwarded-Host $host;\n            proxy_set_header X-Forwarded-Port $server_port;\n\n            # Buffering settings\n            proxy_buffering on;\n            proxy_buffer_size 4k;\n            proxy_buffers 8 4k;\n\n            # Timeouts\n            proxy_connect_timeout 60s;\n            proxy_send_timeout 60s;\n            proxy_read_timeout 60s;\n        }\n    }\n}\n"
  },
  {
    "path": "tests/docker/http2/test_http2_docker.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"HTTP/2 Docker integration tests.\n\nThese tests verify HTTP/2 functionality with real connections to gunicorn\nrunning in Docker containers, both directly and through an nginx proxy.\n\"\"\"\n\nimport asyncio\nimport ssl\nimport socket\n\nimport pytest\n\n\n# Mark all tests in this module as requiring Docker\npytestmark = [\n    pytest.mark.docker,\n    pytest.mark.http2,\n    pytest.mark.integration,\n]\n\n\nclass TestDirectHTTP2Connection:\n    \"\"\"Test direct HTTP/2 connections to gunicorn.\"\"\"\n\n    def test_simple_get(self, h2_client, gunicorn_url):\n        \"\"\"Test basic GET request over HTTP/2.\"\"\"\n        response = h2_client.get(f\"{gunicorn_url}/\")\n        assert response.status_code == 200\n        assert response.http_version == \"HTTP/2\"\n        assert response.text == \"Hello HTTP/2!\"\n\n    def test_health_endpoint(self, h2_client, gunicorn_url):\n        \"\"\"Test health check endpoint.\"\"\"\n        response = h2_client.get(f\"{gunicorn_url}/health\")\n        assert response.status_code == 200\n        assert response.text == \"OK\"\n\n    def test_post_with_body(self, h2_client, gunicorn_url):\n        \"\"\"Test POST request with body.\"\"\"\n        data = b\"test data for echo\"\n        response = h2_client.post(f\"{gunicorn_url}/echo\", content=data)\n        assert response.status_code == 200\n        assert response.content == data\n\n    def test_post_large_body(self, h2_client, gunicorn_url):\n        \"\"\"Test POST with larger body.\"\"\"\n        data = b\"X\" * 65536  # 64KB\n        response = h2_client.post(f\"{gunicorn_url}/echo\", content=data)\n        assert response.status_code == 200\n        assert response.content == data\n        assert len(response.content) == 65536\n\n    def test_headers_endpoint(self, h2_client, gunicorn_url):\n        \"\"\"Test that custom headers are received.\"\"\"\n        response = h2_client.get(\n            f\"{gunicorn_url}/headers\",\n            headers={\"X-Custom-Header\": \"test-value\"}\n        )\n        assert response.status_code == 200\n        headers = response.json()\n        assert \"HTTP_X_CUSTOM_HEADER\" in headers\n        assert headers[\"HTTP_X_CUSTOM_HEADER\"] == \"test-value\"\n\n    def test_version_endpoint(self, h2_client, gunicorn_url):\n        \"\"\"Test server protocol version.\"\"\"\n        response = h2_client.get(f\"{gunicorn_url}/version\")\n        assert response.status_code == 200\n        # HTTP/2 should report as HTTP/2.0 or similar\n        assert \"HTTP\" in response.text\n\n    def test_large_response(self, h2_client, gunicorn_url):\n        \"\"\"Test receiving large response over HTTP/2.\"\"\"\n        response = h2_client.get(f\"{gunicorn_url}/large\")\n        assert response.status_code == 200\n        assert len(response.content) == 1024 * 1024  # 1MB\n        assert response.content == b\"X\" * (1024 * 1024)\n\n    def test_different_methods(self, h2_client, gunicorn_url):\n        \"\"\"Test various HTTP methods.\"\"\"\n        for method in [\"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\"]:\n            response = h2_client.request(method, f\"{gunicorn_url}/method\")\n            assert response.status_code == 200\n            assert response.text == method\n\n    def test_status_codes(self, h2_client, gunicorn_url):\n        \"\"\"Test various HTTP status codes.\"\"\"\n        for code in [200, 201, 400, 404, 500]:\n            response = h2_client.get(f\"{gunicorn_url}/status?code={code}\")\n            assert response.status_code == code\n\n    def test_not_found(self, h2_client, gunicorn_url):\n        \"\"\"Test 404 response.\"\"\"\n        response = h2_client.get(f\"{gunicorn_url}/nonexistent\")\n        assert response.status_code == 404\n\n\nclass TestConcurrentStreams:\n    \"\"\"Test HTTP/2 multiplexing with concurrent streams.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_concurrent_requests(self, async_h2_client, gunicorn_url):\n        \"\"\"Test multiple concurrent requests over single connection.\"\"\"\n        httpx = pytest.importorskip(\"httpx\")\n\n        async with httpx.AsyncClient(http2=True, verify=False, timeout=30.0) as client:\n            # Send 10 concurrent requests\n            tasks = [\n                client.get(f\"{gunicorn_url}/\")\n                for _ in range(10)\n            ]\n            responses = await asyncio.gather(*tasks)\n\n        assert len(responses) == 10\n        assert all(r.status_code == 200 for r in responses)\n        assert all(r.http_version == \"HTTP/2\" for r in responses)\n        assert all(r.text == \"Hello HTTP/2!\" for r in responses)\n\n    @pytest.mark.asyncio\n    async def test_concurrent_mixed_requests(self, async_h2_client, gunicorn_url):\n        \"\"\"Test concurrent requests to different endpoints.\"\"\"\n        httpx = pytest.importorskip(\"httpx\")\n\n        async with httpx.AsyncClient(http2=True, verify=False, timeout=30.0) as client:\n            tasks = [\n                client.get(f\"{gunicorn_url}/\"),\n                client.get(f\"{gunicorn_url}/headers\"),\n                client.get(f\"{gunicorn_url}/version\"),\n                client.post(f\"{gunicorn_url}/echo\", content=b\"test\"),\n                client.get(f\"{gunicorn_url}/health\"),\n            ]\n            responses = await asyncio.gather(*tasks)\n\n        assert len(responses) == 5\n        assert all(r.status_code == 200 for r in responses)\n\n    @pytest.mark.asyncio\n    async def test_many_concurrent_streams(self, async_h2_client, gunicorn_url):\n        \"\"\"Test many concurrent streams (up to HTTP/2 limit).\"\"\"\n        httpx = pytest.importorskip(\"httpx\")\n\n        async with httpx.AsyncClient(http2=True, verify=False, timeout=60.0) as client:\n            # Send 50 concurrent requests\n            tasks = [\n                client.get(f\"{gunicorn_url}/\")\n                for _ in range(50)\n            ]\n            responses = await asyncio.gather(*tasks)\n\n        assert len(responses) == 50\n        assert all(r.status_code == 200 for r in responses)\n\n\nclass TestHTTP2BehindProxy:\n    \"\"\"Test HTTP/2 through nginx proxy.\"\"\"\n\n    def test_simple_get_via_proxy(self, h2_client, nginx_url):\n        \"\"\"Test basic GET through nginx proxy.\"\"\"\n        response = h2_client.get(f\"{nginx_url}/\")\n        assert response.status_code == 200\n        assert response.http_version == \"HTTP/2\"\n        assert response.text == \"Hello HTTP/2!\"\n\n    def test_post_via_proxy(self, h2_client, nginx_url):\n        \"\"\"Test POST through nginx proxy.\"\"\"\n        data = b\"proxied data\"\n        response = h2_client.post(f\"{nginx_url}/echo\", content=data)\n        assert response.status_code == 200\n        assert response.content == data\n\n    def test_headers_preserved(self, h2_client, nginx_url):\n        \"\"\"Test that custom headers pass through proxy.\"\"\"\n        response = h2_client.get(\n            f\"{nginx_url}/headers\",\n            headers={\"X-Custom\": \"test-value\"}\n        )\n        assert response.status_code == 200\n        headers = response.json()\n        assert \"HTTP_X_CUSTOM\" in headers\n        assert headers[\"HTTP_X_CUSTOM\"] == \"test-value\"\n\n    def test_forwarded_headers(self, h2_client, nginx_url):\n        \"\"\"Test that proxy adds forwarded headers.\"\"\"\n        response = h2_client.get(f\"{nginx_url}/headers\")\n        assert response.status_code == 200\n        headers = response.json()\n        # Nginx should add X-Forwarded-* headers\n        assert \"HTTP_X_FORWARDED_FOR\" in headers\n        assert \"HTTP_X_FORWARDED_PROTO\" in headers\n        assert headers[\"HTTP_X_FORWARDED_PROTO\"] == \"https\"\n\n    def test_large_response_via_proxy(self, h2_client, nginx_url):\n        \"\"\"Test large response through proxy.\"\"\"\n        response = h2_client.get(f\"{nginx_url}/large\")\n        assert response.status_code == 200\n        assert len(response.content) == 1024 * 1024\n\n    @pytest.mark.asyncio\n    async def test_concurrent_via_proxy(self, async_h2_client, nginx_url):\n        \"\"\"Test concurrent requests through proxy.\"\"\"\n        httpx = pytest.importorskip(\"httpx\")\n\n        async with httpx.AsyncClient(http2=True, verify=False, timeout=30.0) as client:\n            tasks = [\n                client.get(f\"{nginx_url}/\")\n                for _ in range(10)\n            ]\n            responses = await asyncio.gather(*tasks)\n\n        assert len(responses) == 10\n        assert all(r.status_code == 200 for r in responses)\n        assert all(r.http_version == \"HTTP/2\" for r in responses)\n\n\nclass TestHTTP2Protocol:\n    \"\"\"Test HTTP/2 specific protocol behaviors.\"\"\"\n\n    def test_alpn_negotiation(self, gunicorn_url):\n        \"\"\"Verify ALPN negotiates h2 protocol.\"\"\"\n        ctx = ssl.create_default_context()\n        ctx.check_hostname = False\n        ctx.verify_mode = ssl.CERT_NONE\n        ctx.set_alpn_protocols(['h2', 'http/1.1'])\n\n        with socket.create_connection(('127.0.0.1', 8443), timeout=10) as sock:\n            with ctx.wrap_socket(sock, server_hostname='localhost') as ssock:\n                selected = ssock.selected_alpn_protocol()\n                assert selected == 'h2', f\"Expected h2, got {selected}\"\n\n    def test_alpn_http11_fallback(self, gunicorn_url):\n        \"\"\"Test that server accepts HTTP/1.1 via ALPN.\"\"\"\n        ctx = ssl.create_default_context()\n        ctx.check_hostname = False\n        ctx.verify_mode = ssl.CERT_NONE\n        ctx.set_alpn_protocols(['http/1.1'])\n\n        with socket.create_connection(('127.0.0.1', 8443), timeout=10) as sock:\n            with ctx.wrap_socket(sock, server_hostname='localhost') as ssock:\n                selected = ssock.selected_alpn_protocol()\n                assert selected == 'http/1.1', f\"Expected http/1.1, got {selected}\"\n\n    def test_http11_client_works(self, h1_client, gunicorn_url):\n        \"\"\"Test that HTTP/1.1 client can still connect.\"\"\"\n        response = h1_client.get(f\"{gunicorn_url}/\")\n        assert response.status_code == 200\n        assert response.http_version == \"HTTP/1.1\"\n        assert response.text == \"Hello HTTP/2!\"\n\n    def test_tls_version(self, gunicorn_url):\n        \"\"\"Verify TLS 1.2+ is used.\"\"\"\n        ctx = ssl.create_default_context()\n        ctx.check_hostname = False\n        ctx.verify_mode = ssl.CERT_NONE\n\n        with socket.create_connection(('127.0.0.1', 8443), timeout=10) as sock:\n            with ctx.wrap_socket(sock, server_hostname='localhost') as ssock:\n                version = ssock.version()\n                assert version in ('TLSv1.2', 'TLSv1.3'), f\"Unexpected TLS version: {version}\"\n\n\nclass TestHTTP2ErrorHandling:\n    \"\"\"Test HTTP/2 error handling.\"\"\"\n\n    def test_invalid_path(self, h2_client, gunicorn_url):\n        \"\"\"Test request to non-existent path.\"\"\"\n        response = h2_client.get(f\"{gunicorn_url}/does/not/exist\")\n        assert response.status_code == 404\n        assert response.http_version == \"HTTP/2\"\n\n    def test_server_error(self, h2_client, gunicorn_url):\n        \"\"\"Test server error response.\"\"\"\n        response = h2_client.get(f\"{gunicorn_url}/status?code=500\")\n        assert response.status_code == 500\n        assert response.http_version == \"HTTP/2\"\n\n    @pytest.mark.asyncio\n    async def test_connection_reuse_after_error(self, async_h2_client, gunicorn_url):\n        \"\"\"Test that connection is reused after error response.\"\"\"\n        httpx = pytest.importorskip(\"httpx\")\n\n        async with httpx.AsyncClient(http2=True, verify=False, timeout=30.0) as client:\n            # First request - error\n            r1 = await client.get(f\"{gunicorn_url}/status?code=500\")\n            assert r1.status_code == 500\n\n            # Second request - should work on same connection\n            r2 = await client.get(f\"{gunicorn_url}/\")\n            assert r2.status_code == 200\n            assert r2.text == \"Hello HTTP/2!\"\n\n\nclass TestHTTP2Headers:\n    \"\"\"Test HTTP/2 header handling.\"\"\"\n\n    def test_response_headers(self, h2_client, gunicorn_url):\n        \"\"\"Test that response headers are correctly received.\"\"\"\n        response = h2_client.get(f\"{gunicorn_url}/\")\n        assert \"content-type\" in response.headers\n        assert \"content-length\" in response.headers\n        assert response.headers[\"x-request-path\"] == \"/\"\n        assert response.headers[\"x-request-method\"] == \"GET\"\n\n    def test_many_request_headers(self, h2_client, gunicorn_url):\n        \"\"\"Test sending many headers.\"\"\"\n        headers = {f\"X-Custom-{i}\": f\"value-{i}\" for i in range(20)}\n        response = h2_client.get(f\"{gunicorn_url}/headers\", headers=headers)\n        assert response.status_code == 200\n        received = response.json()\n        for i in range(20):\n            key = f\"HTTP_X_CUSTOM_{i}\"\n            assert key in received\n            assert received[key] == f\"value-{i}\"\n\n    def test_header_case_insensitivity(self, h2_client, gunicorn_url):\n        \"\"\"Test HTTP/2 header case handling.\"\"\"\n        response = h2_client.get(\n            f\"{gunicorn_url}/headers\",\n            headers={\"X-Mixed-Case-Header\": \"test\"}\n        )\n        assert response.status_code == 200\n        # HTTP/2 lowercases headers, but WSGI uppercases them\n        headers = response.json()\n        assert \"HTTP_X_MIXED_CASE_HEADER\" in headers\n\n\nclass TestHTTP2Performance:\n    \"\"\"Performance-related HTTP/2 tests.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_parallel_large_requests(self, async_h2_client, gunicorn_url):\n        \"\"\"Test parallel requests with large responses.\"\"\"\n        httpx = pytest.importorskip(\"httpx\")\n\n        async with httpx.AsyncClient(http2=True, verify=False, timeout=60.0) as client:\n            tasks = [\n                client.get(f\"{gunicorn_url}/large\")\n                for _ in range(5)\n            ]\n            responses = await asyncio.gather(*tasks)\n\n        assert len(responses) == 5\n        assert all(r.status_code == 200 for r in responses)\n        assert all(len(r.content) == 1024 * 1024 for r in responses)\n\n    def test_connection_keepalive(self, h2_client, gunicorn_url):\n        \"\"\"Test that connections are kept alive.\"\"\"\n        # Multiple requests should reuse the same connection\n        for _ in range(5):\n            response = h2_client.get(f\"{gunicorn_url}/\")\n            assert response.status_code == 200\n            assert response.http_version == \"HTTP/2\"\n\n\nclass TestHTTP2EarlyHints:\n    \"\"\"Test HTTP 103 Early Hints support.\"\"\"\n\n    def test_early_hints_endpoint(self, h2_client, gunicorn_url):\n        \"\"\"Test that early hints endpoint returns 200.\"\"\"\n        response = h2_client.get(f\"{gunicorn_url}/early-hints\")\n        assert response.status_code == 200\n        assert response.text == \"Early hints sent!\"\n\n    def test_early_hints_multiple_endpoint(self, h2_client, gunicorn_url):\n        \"\"\"Test multiple early hints endpoint returns 200.\"\"\"\n        response = h2_client.get(f\"{gunicorn_url}/early-hints-multiple\")\n        assert response.status_code == 200\n        assert response.text == \"Multiple early hints sent!\"\n\n    def test_early_hints_via_proxy(self, h2_client, nginx_url):\n        \"\"\"Test early hints through nginx proxy.\"\"\"\n        response = h2_client.get(f\"{nginx_url}/early-hints\")\n        assert response.status_code == 200\n        assert response.text == \"Early hints sent!\"\n\n    @pytest.mark.asyncio\n    async def test_concurrent_early_hints(self, async_h2_client, gunicorn_url):\n        \"\"\"Test concurrent requests to early hints endpoint.\"\"\"\n        httpx = pytest.importorskip(\"httpx\")\n\n        async with httpx.AsyncClient(http2=True, verify=False, timeout=30.0) as client:\n            tasks = [\n                client.get(f\"{gunicorn_url}/early-hints\")\n                for _ in range(10)\n            ]\n            responses = await asyncio.gather(*tasks)\n\n        assert len(responses) == 10\n        assert all(r.status_code == 200 for r in responses)\n        assert all(r.text == \"Early hints sent!\" for r in responses)\n"
  },
  {
    "path": "tests/docker/per_app_allocation/Dockerfile",
    "content": "FROM python:3.12-slim\n\nWORKDIR /app\n\n# Copy gunicorn source\nCOPY . /app/gunicorn-src\n\n# Install gunicorn and test dependencies\n# setproctitle is needed for process title changes (master, dirty-arbiter, etc.)\nRUN pip install --no-cache-dir /app/gunicorn-src pytest requests setproctitle\n\n# Copy test app files\nCOPY tests/docker/per_app_allocation/app.py /app/\nCOPY tests/docker/per_app_allocation/gunicorn_conf.py /app/\n\n# Install procps for process inspection and curl for healthcheck\nRUN apt-get update && apt-get install -y procps curl && rm -rf /var/lib/apt/lists/*\n\n# Default command - run gunicorn\nCMD [\"gunicorn\", \"app:application\", \"-c\", \"gunicorn_conf.py\"]\n"
  },
  {
    "path": "tests/docker/per_app_allocation/README.md",
    "content": "# Per-App Worker Allocation E2E Tests\n\nEnd-to-end Docker-based tests for the per-app worker allocation feature.\n\n## Overview\n\nThese tests verify that:\n- Apps with worker limits are only loaded on the specified number of workers\n- Requests are routed only to workers that have the target app loaded\n- Round-robin distribution works correctly within limited worker sets\n- Worker crash scenarios maintain correct app allocation\n- Class attribute `workers=N` is respected\n- Config-based `:N` overrides class attributes\n\n## Configuration\n\nThe tests use 4 dirty workers with 3 apps:\n- **LightweightApp**: No limit (loads on all 4 workers)\n- **HeavyApp**: `workers=2` class attribute (loads on 2 workers)\n- **ConfigLimitedApp**: `:1` config (loads on 1 worker)\n\n## Running Tests\n\n```bash\n# From this directory\ncd tests/docker/per_app_allocation\n\n# Build the Docker image\ndocker compose build\n\n# Run all tests\npytest test_per_app_e2e.py -v\n\n# Run specific test\npytest test_per_app_e2e.py::TestPerAppAllocation::test_config_limited_app_uses_one_worker -v\n```\n\n## Test Categories\n\n### TestPerAppAllocation\n- Tests basic functionality of per-app worker allocation\n- Verifies round-robin distribution\n- Tests app accessibility\n\n### TestPerAppWorkerCrash\n- Tests behavior when workers crash\n- Verifies app recovery after worker respawn\n\n### TestPerAppLogs\n- Verifies logging output contains expected information\n\n## Requirements\n\n- Docker and Docker Compose\n- Python 3.8+\n- pytest\n- requests\n\n## Notes\n\n- Tests run on port 8001 to avoid conflicts with the existing dirty_arbiter tests on 8000\n- The container uses a keep-alive wrapper to allow testing worker crash scenarios\n"
  },
  {
    "path": "tests/docker/per_app_allocation/app.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nWSGI and Dirty applications for per-app worker allocation testing.\n\nContains:\n- A WSGI app that can make dirty client requests\n- A lightweight dirty app (loads on all workers)\n- A heavy dirty app (limited to 2 workers via class attribute)\n- A config-limited app (limited to 1 worker via config)\n\"\"\"\n\nimport json\nimport os\n\nfrom gunicorn.dirty.app import DirtyApp\n\n\ndef application(environ, start_response):\n    \"\"\"\n    WSGI application that invokes dirty apps and returns worker info.\n\n    Routes:\n    - GET /lightweight/ping - Call LightweightApp.ping()\n    - GET /heavy/predict/<data> - Call HeavyApp.predict(data)\n    - GET /config_limited/info - Call ConfigLimitedApp.get_info()\n    - GET /status - Get overall status\n    \"\"\"\n    path = environ.get('PATH_INFO', '/')\n    method = environ.get('REQUEST_METHOD', 'GET')\n\n    if method != 'GET':\n        start_response('405 Method Not Allowed', [('Content-Type', 'text/plain')])\n        return [b'Method not allowed']\n\n    # Import dirty client here to avoid import at module load\n    from gunicorn.dirty import get_dirty_client\n\n    try:\n        client = get_dirty_client()\n\n        if path == '/status':\n            start_response('200 OK', [('Content-Type', 'application/json')])\n            return [json.dumps({\"status\": \"ok\"}).encode()]\n\n        elif path == '/lightweight/ping':\n            result = client.execute(\"app:LightweightApp\", \"ping\")\n            start_response('200 OK', [('Content-Type', 'application/json')])\n            return [json.dumps(result).encode()]\n\n        elif path.startswith('/heavy/predict/'):\n            data = path.split('/')[-1]\n            result = client.execute(\"app:HeavyApp\", \"predict\", data)\n            start_response('200 OK', [('Content-Type', 'application/json')])\n            return [json.dumps(result).encode()]\n\n        elif path == '/heavy/get_worker_id':\n            result = client.execute(\"app:HeavyApp\", \"get_worker_id\")\n            start_response('200 OK', [('Content-Type', 'application/json')])\n            return [json.dumps({\"worker_id\": result}).encode()]\n\n        elif path == '/config_limited/info':\n            result = client.execute(\"app:ConfigLimitedApp\", \"get_info\")\n            start_response('200 OK', [('Content-Type', 'application/json')])\n            return [json.dumps(result).encode()]\n\n        elif path == '/config_limited/get_worker_id':\n            result = client.execute(\"app:ConfigLimitedApp\", \"get_worker_id\")\n            start_response('200 OK', [('Content-Type', 'application/json')])\n            return [json.dumps({\"worker_id\": result}).encode()]\n\n        elif path == '/lightweight/get_worker_id':\n            result = client.execute(\"app:LightweightApp\", \"get_worker_id\")\n            start_response('200 OK', [('Content-Type', 'application/json')])\n            return [json.dumps({\"worker_id\": result}).encode()]\n\n        else:\n            start_response('404 Not Found', [('Content-Type', 'text/plain')])\n            return [b'Not found']\n\n    except Exception as e:\n        start_response('500 Internal Server Error', [('Content-Type', 'application/json')])\n        return [json.dumps({\"error\": str(e), \"type\": type(e).__name__}).encode()]\n\n\nclass LightweightApp(DirtyApp):\n    \"\"\"\n    A lightweight app that should load on ALL dirty workers.\n\n    workers=None (default) means all workers load this app.\n    \"\"\"\n\n    def __init__(self):\n        self.initialized = False\n        self.worker_id = None\n        self.call_count = 0\n\n    def init(self):\n        self.initialized = True\n        self.worker_id = os.getpid()\n\n    def ping(self):\n        \"\"\"Simple ping action.\"\"\"\n        self.call_count += 1\n        return {\n            \"pong\": True,\n            \"worker_id\": self.worker_id,\n            \"call_count\": self.call_count,\n        }\n\n    def get_worker_id(self):\n        \"\"\"Return the worker ID that loaded this app.\"\"\"\n        return self.worker_id\n\n    def close(self):\n        pass\n\n\nclass HeavyApp(DirtyApp):\n    \"\"\"\n    A heavy app that uses the workers class attribute to limit allocation.\n\n    workers=2 means only 2 dirty workers will load this app.\n    This simulates a large ML model that shouldn't be replicated everywhere.\n    \"\"\"\n    workers = 2  # Only 2 workers should load this app\n\n    def __init__(self):\n        self.initialized = False\n        self.worker_id = None\n        self.model_data = None\n\n    def init(self):\n        self.initialized = True\n        self.worker_id = os.getpid()\n        # Simulate loading a heavy model\n        self.model_data = {\"loaded\": True, \"worker\": self.worker_id}\n\n    def predict(self, data):\n        \"\"\"Simulate model prediction.\"\"\"\n        return {\n            \"prediction\": f\"result_for_{data}\",\n            \"worker_id\": self.worker_id,\n        }\n\n    def get_worker_id(self):\n        \"\"\"Return the worker ID that loaded this app.\"\"\"\n        return self.worker_id\n\n    def close(self):\n        self.model_data = None\n\n\nclass ConfigLimitedApp(DirtyApp):\n    \"\"\"\n    An app whose worker limit is specified in config (not class attribute).\n\n    The config will specify this app as \"app:ConfigLimitedApp:1\" to limit\n    it to a single worker.\n    \"\"\"\n\n    def __init__(self):\n        self.initialized = False\n        self.worker_id = None\n\n    def init(self):\n        self.initialized = True\n        self.worker_id = os.getpid()\n\n    def get_info(self):\n        \"\"\"Get app info.\"\"\"\n        return {\n            \"app\": \"ConfigLimitedApp\",\n            \"worker_id\": self.worker_id,\n        }\n\n    def get_worker_id(self):\n        \"\"\"Return the worker ID that loaded this app.\"\"\"\n        return self.worker_id\n\n    def close(self):\n        pass\n"
  },
  {
    "path": "tests/docker/per_app_allocation/docker-compose.yml",
    "content": "services:\n  gunicorn:\n    build:\n      context: ../../..\n      dockerfile: tests/docker/per_app_allocation/Dockerfile\n    ports:\n      - \"8001:8000\"\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:8000/status\"]\n      interval: 1s\n      timeout: 1s\n      retries: 30\n    stop_grace_period: 10s\n"
  },
  {
    "path": "tests/docker/per_app_allocation/gunicorn_conf.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nGunicorn configuration for per-app worker allocation e2e tests.\n\nConfiguration:\n- 4 dirty workers total\n- LightweightApp: loads on ALL 4 workers (workers=None)\n- HeavyApp: loads on 2 workers (via class attribute workers=2)\n- ConfigLimitedApp: loads on 1 worker (via config :1 suffix)\n\"\"\"\n\nbind = \"0.0.0.0:8000\"\nworkers = 1  # HTTP workers\nworker_class = \"sync\"\n\n# 4 dirty workers - enough to test distribution\ndirty_workers = 4\n\n# App configuration:\n# - LightweightApp: no limit, loads on all 4\n# - HeavyApp: workers=2 class attribute, loads on 2\n# - ConfigLimitedApp: config override :1, loads on 1\ndirty_apps = [\n    \"app:LightweightApp\",\n    \"app:HeavyApp\",\n    \"app:ConfigLimitedApp:1\",\n]\n\ndirty_timeout = 30\ndirty_graceful_timeout = 5\ntimeout = 30\ngraceful_timeout = 5\nloglevel = \"debug\"\naccesslog = \"-\"\nerrorlog = \"-\"\n"
  },
  {
    "path": "tests/docker/per_app_allocation/test_per_app_e2e.py",
    "content": "#!/usr/bin/env python\n#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nDocker-based end-to-end tests for per-app worker allocation.\n\nThese tests verify:\n1. Apps with worker limits are only loaded on limited workers\n2. Requests are routed to workers that have the target app\n3. Round-robin distribution works within limited worker sets\n4. Worker crash scenarios maintain correct app allocation\n\nUsage:\n    # Build the container first\n    docker compose build\n\n    # Run all tests\n    pytest test_per_app_e2e.py -v\n\n    # Run specific test\n    pytest test_per_app_e2e.py::TestPerAppAllocation::test_lightweight_app_round_robins -v\n\"\"\"\n\nimport os\nimport re\nimport subprocess\nimport time\n\nimport pytest\nimport requests\n\n\nclass DockerContainer:\n    \"\"\"Context manager for managing a Docker container for per-app tests.\"\"\"\n\n    def __init__(self, name=\"gunicorn-per-app-test\", build=True):\n        self.name = name\n        self.build = build\n        self.container_id = None\n        self.base_url = \"http://127.0.0.1:8001\"\n\n    def __enter__(self):\n        # Build if requested\n        if self.build:\n            result = subprocess.run(\n                [\"docker\", \"compose\", \"build\"],\n                cwd=os.path.dirname(__file__),\n                capture_output=True,\n                text=True,\n            )\n            if result.returncode != 0:\n                raise RuntimeError(f\"Docker build failed: {result.stderr}\")\n\n        # Remove any existing container with same name\n        subprocess.run(\n            [\"docker\", \"rm\", \"-f\", self.name],\n            capture_output=True,\n        )\n\n        # Start container with a keep-alive wrapper\n        result = subprocess.run(\n            [\n                \"docker\", \"run\", \"-d\",\n                \"--name\", self.name,\n                \"-p\", \"8001:8000\",\n                \"per_app_allocation-gunicorn\",\n                \"sh\", \"-c\",\n                \"gunicorn app:application -c gunicorn_conf.py & \"\n                \"GUNICORN_PID=$!; \"\n                \"trap 'kill $GUNICORN_PID 2>/dev/null' TERM; \"\n                \"while true; do sleep 1; done\"\n            ],\n            capture_output=True,\n            text=True,\n        )\n        if result.returncode != 0:\n            raise RuntimeError(f\"Docker run failed: {result.stderr}\")\n\n        self.container_id = result.stdout.strip()\n\n        # Wait for gunicorn to be ready\n        self._wait_for_ready()\n\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        if self.container_id:\n            # Get logs before cleanup\n            logs = self.get_logs()\n            if exc_val:\n                print(f\"\\n=== Container logs ===\\n{logs}\\n=== End logs ===\\n\")\n\n            # Stop and remove container\n            subprocess.run(\n                [\"docker\", \"rm\", \"-f\", self.name],\n                capture_output=True,\n            )\n\n    def _wait_for_ready(self, timeout=60):\n        \"\"\"Wait for gunicorn to be ready and serving requests.\"\"\"\n        start = time.time()\n        while time.time() - start < timeout:\n            try:\n                resp = requests.get(f\"{self.base_url}/status\", timeout=1)\n                if resp.status_code == 200:\n                    # Also verify dirty workers are up by testing an app\n                    resp = requests.get(f\"{self.base_url}/lightweight/ping\", timeout=2)\n                    if resp.status_code == 200:\n                        return\n            except requests.exceptions.RequestException:\n                pass\n            time.sleep(0.5)\n        raise TimeoutError(\"Gunicorn did not start within timeout\")\n\n    def exec(self, cmd, check=True):\n        \"\"\"Execute a command in the container.\"\"\"\n        result = subprocess.run(\n            [\"docker\", \"exec\", self.name] + cmd,\n            capture_output=True,\n            text=True,\n        )\n        if check and result.returncode != 0:\n            raise RuntimeError(f\"Command failed: {cmd}\\n{result.stderr}\")\n        return result\n\n    def get_logs(self):\n        \"\"\"Get container logs.\"\"\"\n        result = subprocess.run(\n            [\"docker\", \"logs\", self.name],\n            capture_output=True,\n            text=True,\n        )\n        return result.stdout + result.stderr\n\n    def get_gunicorn_pids(self):\n        \"\"\"Get PIDs of gunicorn processes.\"\"\"\n        pids = {\n            \"master\": None,\n            \"dirty-arbiter\": None,\n            \"workers\": [],\n            \"dirty-workers\": [],\n        }\n\n        result = self.exec([\"ps\", \"aux\"], check=False)\n\n        for line in result.stdout.split(\"\\n\"):\n            if \"gunicorn:\" not in line:\n                continue\n\n            parts = line.split()\n            if len(parts) < 2:\n                continue\n\n            pid = int(parts[1])\n\n            if \"gunicorn: master\" in line:\n                pids[\"master\"] = pid\n            elif \"gunicorn: dirty-arbiter\" in line:\n                pids[\"dirty-arbiter\"] = pid\n            elif \"gunicorn: dirty-worker\" in line:\n                pids[\"dirty-workers\"].append(pid)\n            elif \"gunicorn: worker\" in line:\n                pids[\"workers\"].append(pid)\n\n        return pids\n\n    def kill_process(self, pid, signal=9):\n        \"\"\"Send a signal to a process in the container.\"\"\"\n        self.exec(\n            [\"kill\", f\"-{signal}\", str(pid)],\n            check=False,\n        )\n\n    def wait_for_dirty_worker_count(self, expected_count, timeout=10):\n        \"\"\"Wait for specific number of dirty workers.\"\"\"\n        start = time.time()\n        while time.time() - start < timeout:\n            pids = self.get_gunicorn_pids()\n            if len(pids[\"dirty-workers\"]) == expected_count:\n                return True\n            time.sleep(0.5)\n        return False\n\n    def http_get(self, path, timeout=5):\n        \"\"\"Make HTTP GET request to the container.\"\"\"\n        return requests.get(f\"{self.base_url}{path}\", timeout=timeout)\n\n\nclass TestPerAppAllocation:\n    \"\"\"Test per-app worker allocation functionality.\"\"\"\n\n    @pytest.fixture(autouse=True)\n    def setup(self):\n        \"\"\"Check Docker is available.\"\"\"\n        result = subprocess.run(\n            [\"docker\", \"info\"],\n            capture_output=True,\n        )\n        if result.returncode != 0:\n            pytest.skip(\"Docker is not available\")\n\n    def test_lightweight_app_responds(self):\n        \"\"\"LightweightApp should be accessible and respond correctly.\"\"\"\n        with DockerContainer() as container:\n            resp = container.http_get(\"/lightweight/ping\")\n            assert resp.status_code == 200\n\n            data = resp.json()\n            assert data[\"pong\"] is True\n            assert \"worker_id\" in data\n\n    def test_lightweight_app_round_robins(self):\n        \"\"\"LightweightApp requests should round-robin across all 4 workers.\"\"\"\n        with DockerContainer() as container:\n            # Make multiple requests to collect worker IDs\n            worker_ids = set()\n            for _ in range(20):  # More than 4 to ensure round-robin\n                resp = container.http_get(\"/lightweight/get_worker_id\")\n                assert resp.status_code == 200\n                data = resp.json()\n                worker_ids.add(data[\"worker_id\"])\n\n            # Should see all 4 workers (or at least more than 1)\n            # Note: Due to timing, we might not hit all 4 in exactly 20 requests\n            assert len(worker_ids) >= 2, (\n                f\"Expected requests to go to multiple workers, got {len(worker_ids)}\"\n            )\n\n    def test_config_limited_app_uses_one_worker(self):\n        \"\"\"ConfigLimitedApp (limited to 1 via config) should use only one worker.\"\"\"\n        with DockerContainer() as container:\n            # Make multiple requests\n            worker_ids = set()\n            for _ in range(10):\n                resp = container.http_get(\"/config_limited/get_worker_id\")\n                assert resp.status_code == 200\n                data = resp.json()\n                worker_ids.add(data[\"worker_id\"])\n\n            # Should only see 1 worker (the app is limited to 1)\n            assert len(worker_ids) == 1, (\n                f\"Expected ConfigLimitedApp to use only 1 worker, got {len(worker_ids)}\"\n            )\n\n    def test_heavy_app_uses_limited_workers(self):\n        \"\"\"HeavyApp (workers=2) should use only 2 workers.\"\"\"\n        with DockerContainer() as container:\n            # Make multiple requests\n            worker_ids = set()\n            for _ in range(20):\n                resp = container.http_get(\"/heavy/get_worker_id\")\n                # HeavyApp uses class attribute workers=2\n                # But currently the arbiter only reads config :N format\n                # This test documents expected behavior\n                if resp.status_code == 200:\n                    data = resp.json()\n                    worker_ids.add(data[\"worker_id\"])\n                else:\n                    # If class attribute isn't supported yet, skip\n                    pytest.skip(\"HeavyApp class attribute workers=2 not implemented\")\n                    return\n\n            # Should see at most 2 workers\n            assert len(worker_ids) <= 2, (\n                f\"Expected HeavyApp to use at most 2 workers, got {len(worker_ids)}\"\n            )\n\n    def test_heavy_app_prediction_works(self):\n        \"\"\"HeavyApp.predict() should return correct results.\"\"\"\n        with DockerContainer() as container:\n            resp = container.http_get(\"/heavy/predict/test_input\")\n\n            if resp.status_code == 200:\n                data = resp.json()\n                assert data[\"prediction\"] == \"result_for_test_input\"\n                assert \"worker_id\" in data\n            else:\n                # If class attribute isn't supported, document the error\n                data = resp.json()\n                print(f\"HeavyApp error: {data}\")\n\n    def test_all_apps_accessible(self):\n        \"\"\"All configured apps should be accessible.\"\"\"\n        with DockerContainer() as container:\n            # LightweightApp\n            resp = container.http_get(\"/lightweight/ping\")\n            assert resp.status_code == 200\n\n            # ConfigLimitedApp\n            resp = container.http_get(\"/config_limited/info\")\n            assert resp.status_code == 200\n            data = resp.json()\n            assert data[\"app\"] == \"ConfigLimitedApp\"\n\n    def test_four_dirty_workers_running(self):\n        \"\"\"Should have 4 dirty workers as configured.\"\"\"\n        with DockerContainer() as container:\n            pids = container.get_gunicorn_pids()\n\n            assert len(pids[\"dirty-workers\"]) == 4, (\n                f\"Expected 4 dirty workers, got {len(pids['dirty-workers'])}\"\n            )\n\n\nclass TestPerAppWorkerCrash:\n    \"\"\"Test per-app allocation behavior when workers crash.\"\"\"\n\n    @pytest.fixture(autouse=True)\n    def setup(self):\n        \"\"\"Check Docker is available.\"\"\"\n        result = subprocess.run(\n            [\"docker\", \"info\"],\n            capture_output=True,\n        )\n        if result.returncode != 0:\n            pytest.skip(\"Docker is not available\")\n\n    def test_worker_crash_app_still_accessible(self):\n        \"\"\"When a dirty worker crashes, apps should still be accessible.\"\"\"\n        with DockerContainer() as container:\n            pids = container.get_gunicorn_pids()\n            assert len(pids[\"dirty-workers\"]) == 4\n\n            # Kill one dirty worker\n            container.kill_process(pids[\"dirty-workers\"][0], signal=9)\n\n            # Wait for respawn (dirty arbiter should respawn it)\n            assert container.wait_for_dirty_worker_count(4, timeout=15), (\n                \"Dirty arbiter should respawn killed worker\"\n            )\n\n            # Apps should still work\n            resp = container.http_get(\"/lightweight/ping\")\n            assert resp.status_code == 200\n\n            resp = container.http_get(\"/config_limited/info\")\n            assert resp.status_code == 200\n\n    def test_config_limited_worker_crash_recovery(self):\n        \"\"\"When the sole worker for ConfigLimitedApp crashes, it should recover.\"\"\"\n        with DockerContainer() as container:\n            # Get the worker ID that handles ConfigLimitedApp\n            resp = container.http_get(\"/config_limited/get_worker_id\")\n            assert resp.status_code == 200\n            original_worker_id = resp.json()[\"worker_id\"]\n\n            # Kill that specific worker\n            container.kill_process(original_worker_id, signal=9)\n\n            # Wait for respawn\n            time.sleep(3)\n\n            # The new worker should handle ConfigLimitedApp\n            resp = container.http_get(\"/config_limited/get_worker_id\")\n            # Note: There might be a brief period where no worker has the app\n            # In production, this would return an error until respawn\n            if resp.status_code == 200:\n                new_worker_id = resp.json()[\"worker_id\"]\n                # Worker ID should be different (new process)\n                assert new_worker_id != original_worker_id, (\n                    \"New worker should have different PID\"\n                )\n\n\nclass TestPerAppLogs:\n    \"\"\"Test that per-app allocation is logged correctly.\"\"\"\n\n    @pytest.fixture(autouse=True)\n    def setup(self):\n        \"\"\"Check Docker is available.\"\"\"\n        result = subprocess.run(\n            [\"docker\", \"info\"],\n            capture_output=True,\n        )\n        if result.returncode != 0:\n            pytest.skip(\"Docker is not available\")\n\n    def test_logs_show_app_allocation(self):\n        \"\"\"Logs should indicate which apps are loaded on which workers.\"\"\"\n        with DockerContainer() as container:\n            logs = container.get_logs()\n\n            # Should see dirty arbiter starting\n            assert \"Dirty arbiter\" in logs or \"dirty arbiter\" in logs.lower()\n\n            # Should see dirty workers spawning\n            assert \"dirty\" in logs.lower() and \"worker\" in logs.lower()\n\n\nif __name__ == \"__main__\":\n    pytest.main([__file__, \"-v\"])\n"
  },
  {
    "path": "tests/docker/test_asgi_uwsgi/Dockerfile",
    "content": "FROM python:3.11-slim\n\nWORKDIR /build\n\n# Copy gunicorn source\nCOPY . /build/\n\n# Install gunicorn from source\nRUN pip install --no-cache-dir -e .\n\n# Copy test app\nWORKDIR /app\nCOPY tests/docker/test_asgi_uwsgi/app.py /app/\n\n# Expose uWSGI port\nEXPOSE 8000\n\nCMD [\"gunicorn\", \"--worker-class\", \"asgi\", \"--protocol\", \"uwsgi\", \"--bind\", \"0.0.0.0:8000\", \"app:app\"]\n"
  },
  {
    "path": "tests/docker/test_asgi_uwsgi/app.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Simple ASGI test application for uWSGI protocol testing.\"\"\"\n\n\nasync def app(scope, receive, send):\n    \"\"\"Simple ASGI application that echoes request info.\"\"\"\n    if scope[\"type\"] == \"lifespan\":\n        while True:\n            message = await receive()\n            if message[\"type\"] == \"lifespan.startup\":\n                await send({\"type\": \"lifespan.startup.complete\"})\n            elif message[\"type\"] == \"lifespan.shutdown\":\n                await send({\"type\": \"lifespan.shutdown.complete\"})\n                return\n\n    if scope[\"type\"] != \"http\":\n        return\n\n    # Read body\n    body = b\"\"\n    while True:\n        message = await receive()\n        body += message.get(\"body\", b\"\")\n        if not message.get(\"more_body\", False):\n            break\n\n    # Build response\n    method = scope[\"method\"]\n    path = scope[\"path\"]\n    query = scope.get(\"query_string\", b\"\").decode(\"utf-8\")\n\n    response_body = f\"Method: {method}\\nPath: {path}\\nQuery: {query}\\nBody: {body.decode('utf-8')}\\n\"\n    response_bytes = response_body.encode(\"utf-8\")\n\n    await send({\n        \"type\": \"http.response.start\",\n        \"status\": 200,\n        \"headers\": [\n            [b\"content-type\", b\"text/plain\"],\n            [b\"content-length\", str(len(response_bytes)).encode()],\n        ],\n    })\n    await send({\n        \"type\": \"http.response.body\",\n        \"body\": response_bytes,\n    })\n"
  },
  {
    "path": "tests/docker/test_asgi_uwsgi/docker-compose.yml",
    "content": "services:\n  gunicorn:\n    build:\n      context: ../../..\n      dockerfile: tests/docker/test_asgi_uwsgi/Dockerfile\n    command: >\n      gunicorn\n      --worker-class asgi\n      --protocol uwsgi\n      --uwsgi-allow-from '*'\n      --bind 0.0.0.0:8000\n      --workers 1\n      --log-level debug\n      app:app\n    working_dir: /app\n\n  nginx:\n    image: nginx:alpine\n    ports:\n      - \"8080:80\"\n    volumes:\n      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro\n    depends_on:\n      - gunicorn\n"
  },
  {
    "path": "tests/docker/test_asgi_uwsgi/nginx.conf",
    "content": "server {\n    listen 80;\n    server_name localhost;\n\n    location / {\n        uwsgi_pass gunicorn:8000;\n        include uwsgi_params;\n    }\n\n    location /health {\n        return 200 \"OK\";\n        add_header Content-Type text/plain;\n    }\n}\n"
  },
  {
    "path": "tests/docker/test_asgi_uwsgi/test_uwsgi.sh",
    "content": "#!/bin/bash\n# Integration test for ASGI uWSGI protocol support\n#\n# This script tests that gunicorn's ASGI worker correctly handles\n# the uWSGI protocol when nginx forwards requests using uwsgi_pass.\n\nset -e\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\ncd \"$SCRIPT_DIR\"\n\n# Use IPv4 explicitly to avoid Docker IPv6 issues\nBASE_URL=\"http://127.0.0.1:8080\"\n\ncleanup() {\n    echo \"Cleaning up...\"\n    docker compose down -v 2>/dev/null || true\n}\n\ntrap cleanup EXIT\n\necho \"=== Building and starting containers ===\"\ndocker compose up -d --build\n\necho \"=== Waiting for services to be ready ===\"\nsleep 5\n\necho \"=== Running tests ===\"\n\n# Test 1: Simple GET request\necho \"Test 1: Simple GET request\"\nRESPONSE=$(curl -s \"$BASE_URL/\")\nif echo \"$RESPONSE\" | grep -q \"Method: GET\"; then\n    echo \"  PASS: GET request works\"\nelse\n    echo \"  FAIL: GET request failed\"\n    echo \"  Response: $RESPONSE\"\n    exit 1\nfi\n\n# Test 2: GET with query string\necho \"Test 2: GET with query string\"\nRESPONSE=$(curl -s \"$BASE_URL/search?q=test&page=1\")\nif echo \"$RESPONSE\" | grep -q \"Query: q=test&page=1\"; then\n    echo \"  PASS: Query string works\"\nelse\n    echo \"  FAIL: Query string failed\"\n    echo \"  Response: $RESPONSE\"\n    exit 1\nfi\n\n# Test 3: POST with body\necho \"Test 3: POST with body\"\nRESPONSE=$(curl -s -X POST -d \"hello=world\" \"$BASE_URL/submit\")\nif echo \"$RESPONSE\" | grep -q \"Method: POST\" && echo \"$RESPONSE\" | grep -q \"Body: hello=world\"; then\n    echo \"  PASS: POST with body works\"\nelse\n    echo \"  FAIL: POST with body failed\"\n    echo \"  Response: $RESPONSE\"\n    exit 1\nfi\n\n# Test 4: Path handling\necho \"Test 4: Path handling\"\nRESPONSE=$(curl -s \"$BASE_URL/api/v1/users\")\nif echo \"$RESPONSE\" | grep -q \"Path: /api/v1/users\"; then\n    echo \"  PASS: Path handling works\"\nelse\n    echo \"  FAIL: Path handling failed\"\n    echo \"  Response: $RESPONSE\"\n    exit 1\nfi\n\n# Test 5: Multiple requests (keepalive)\necho \"Test 5: Multiple requests (keepalive)\"\nfor i in 1 2 3; do\n    RESPONSE=$(curl -s \"$BASE_URL/request/$i\")\n    if ! echo \"$RESPONSE\" | grep -q \"Path: /request/$i\"; then\n        echo \"  FAIL: Request $i failed\"\n        exit 1\n    fi\ndone\necho \"  PASS: Multiple requests work\"\n\necho \"\"\necho \"=== All tests passed! ===\"\n"
  },
  {
    "path": "tests/docker/uwsgi/Dockerfile.gunicorn",
    "content": "FROM python:3.11-slim\n\nWORKDIR /app\n\n# Copy gunicorn source\nCOPY . /app/gunicorn-src/\n\n# Install gunicorn from source\nRUN pip install --no-cache-dir /app/gunicorn-src/\n\n# Copy test application\nCOPY tests/docker/uwsgi/app.py /app/\n\nEXPOSE 8000\n\nCMD [\"gunicorn\", \"--protocol\", \"uwsgi\", \"--uwsgi-allow-from\", \"*\", \"--bind\", \"0.0.0.0:8000\", \"--workers\", \"2\", \"--log-level\", \"debug\", \"app:application\"]\n"
  },
  {
    "path": "tests/docker/uwsgi/Dockerfile.nginx",
    "content": "FROM nginx:alpine\n\n# Remove default config\nRUN rm /etc/nginx/conf.d/default.conf\n\n# Copy custom config\nCOPY nginx.conf /etc/nginx/nginx.conf\nCOPY uwsgi_params /etc/nginx/uwsgi_params\n\nEXPOSE 8080\n\nCMD [\"nginx\", \"-g\", \"daemon off;\"]\n"
  },
  {
    "path": "tests/docker/uwsgi/README.md",
    "content": "# uWSGI Protocol Docker Integration Tests\n\nThis directory contains Docker-based integration tests that verify gunicorn's\nuWSGI binary protocol implementation works correctly with nginx's `uwsgi_pass`\ndirective.\n\n## Architecture\n\n```\n[pytest] --HTTP--> [nginx:8080] --uwsgi_pass--> [gunicorn:8000]\n```\n\nThe tests make HTTP requests to nginx, which proxies them to gunicorn using the\nuWSGI binary protocol. This validates the complete request/response cycle through\nthe protocol.\n\n## Prerequisites\n\n- Docker\n- Docker Compose (v2)\n- Python 3.8+\n- pytest\n- requests\n\n## Running Tests\n\n### From repository root:\n\n```bash\n# Run all uWSGI integration tests\npytest tests/docker/uwsgi/ -v\n\n# Run specific test class\npytest tests/docker/uwsgi/ -v -k TestBasicRequests\n\n# Skip Docker tests (for CI environments without Docker)\npytest tests/ -v -m \"not docker\"\n```\n\n### Manual testing:\n\n```bash\ncd tests/docker/uwsgi\n\n# Start services\ndocker compose up -d\n\n# Wait for services to be healthy\ndocker compose ps\n\n# Test endpoints\ncurl http://localhost:8080/\ncurl -X POST -d \"test body\" http://localhost:8080/echo\ncurl http://localhost:8080/headers\ncurl \"http://localhost:8080/query?foo=bar\"\ncurl http://localhost:8080/environ\ncurl http://localhost:8080/error/404\ncurl http://localhost:8080/large > /dev/null  # 1MB response\n\n# View logs\ndocker compose logs gunicorn\ndocker compose logs nginx\n\n# Stop services\ndocker compose down -v\n```\n\n## Test Categories\n\n| Category | Description |\n|----------|-------------|\n| `TestBasicRequests` | GET, POST, query strings, large bodies |\n| `TestHeaderPreservation` | Custom headers, Host, Content-Type, User-Agent |\n| `TestKeepAlive` | Multiple requests per connection |\n| `TestErrorResponses` | HTTP error codes (400, 404, 500, etc.) |\n| `TestEnvironVariables` | WSGI environ: REQUEST_METHOD, PATH_INFO, etc. |\n| `TestLargeResponses` | 1MB response body streaming |\n| `TestConcurrency` | Parallel request handling |\n| `TestSpecialCases` | Edge cases: binary data, unicode, long headers |\n\n## Files\n\n| File | Purpose |\n|------|---------|\n| `docker-compose.yml` | Orchestrates nginx + gunicorn containers |\n| `Dockerfile.gunicorn` | Builds gunicorn image with test app |\n| `Dockerfile.nginx` | Builds nginx with uwsgi config |\n| `nginx.conf` | nginx configuration using `uwsgi_pass` |\n| `uwsgi_params` | Standard uwsgi parameter mappings |\n| `app.py` | Test WSGI application with multiple endpoints |\n| `conftest.py` | pytest fixtures for Docker lifecycle |\n| `test_uwsgi_integration.py` | Test cases |\n\n## Test App Endpoints\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| `/` | GET | Basic hello response |\n| `/echo` | POST | Echo request body |\n| `/headers` | GET/POST | Return received headers as JSON |\n| `/environ` | GET/POST | Return WSGI environ as JSON |\n| `/query` | GET | Return query params as JSON |\n| `/json` | POST | Parse and echo JSON body |\n| `/error/{code}` | GET | Return specified HTTP error |\n| `/large` | GET | Return 1MB response |\n\n## Gunicorn Configuration\n\nThe gunicorn container runs with:\n\n```bash\ngunicorn \\\n  --protocol uwsgi \\\n  --uwsgi-allow-from \"*\" \\\n  --bind 0.0.0.0:8000 \\\n  --workers 2 \\\n  --log-level debug \\\n  app:application\n```\n\nKey settings:\n- `--protocol uwsgi`: Enable uWSGI binary protocol\n- `--uwsgi-allow-from \"*\"`: Accept connections from Docker network IPs\n\n## Troubleshooting\n\n### Services won't start\n\nCheck Docker logs:\n```bash\ndocker compose logs\n```\n\n### Connection refused\n\nWait for health checks:\n```bash\ndocker compose ps  # Check health status\n```\n\n### Tests timing out\n\nIncrease `STARTUP_TIMEOUT` in `conftest.py` or check if ports are in use:\n```bash\nlsof -i :8080\nlsof -i :8000\n```\n\n### Rebuild after code changes\n\n```bash\ndocker compose build --no-cache\ndocker compose up -d\n```\n"
  },
  {
    "path": "tests/docker/uwsgi/app.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nTest WSGI application for uWSGI protocol integration tests.\n\nThis application provides various endpoints to test different aspects\nof the uWSGI binary protocol when proxied through nginx.\n\"\"\"\n\nimport json\n\n\ndef application(environ, start_response):\n    \"\"\"Main WSGI application entry point.\"\"\"\n    path = environ.get('PATH_INFO', '/')\n    method = environ.get('REQUEST_METHOD', 'GET')\n\n    # Route to appropriate handler\n    if path == '/':\n        return handle_root(environ, start_response)\n    elif path == '/echo':\n        return handle_echo(environ, start_response)\n    elif path == '/headers':\n        return handle_headers(environ, start_response)\n    elif path == '/environ':\n        return handle_environ(environ, start_response)\n    elif path.startswith('/error/'):\n        return handle_error(environ, start_response, path)\n    elif path == '/large':\n        return handle_large(environ, start_response)\n    elif path == '/json':\n        return handle_json(environ, start_response)\n    elif path == '/query':\n        return handle_query(environ, start_response)\n    else:\n        return handle_not_found(environ, start_response)\n\n\ndef handle_root(environ, start_response):\n    \"\"\"Basic root endpoint.\"\"\"\n    status = '200 OK'\n    headers = [('Content-Type', 'text/plain')]\n    start_response(status, headers)\n    return [b'Hello from gunicorn uWSGI!\\n']\n\n\ndef handle_echo(environ, start_response):\n    \"\"\"Echo back the request body.\"\"\"\n    try:\n        content_length = int(environ.get('CONTENT_LENGTH', 0))\n    except (ValueError, TypeError):\n        content_length = 0\n\n    body = b''\n    if content_length > 0:\n        body = environ['wsgi.input'].read(content_length)\n\n    status = '200 OK'\n    headers = [\n        ('Content-Type', 'application/octet-stream'),\n        ('Content-Length', str(len(body)))\n    ]\n    start_response(status, headers)\n    return [body]\n\n\ndef handle_headers(environ, start_response):\n    \"\"\"Return received HTTP headers as JSON.\"\"\"\n    headers_dict = {}\n    for key, value in environ.items():\n        if key.startswith('HTTP_'):\n            # Convert HTTP_X_CUSTOM_HEADER to X-Custom-Header\n            header_name = key[5:].replace('_', '-').title()\n            headers_dict[header_name] = value\n\n    # Also include some special headers\n    if 'CONTENT_TYPE' in environ:\n        headers_dict['Content-Type'] = environ['CONTENT_TYPE']\n    if 'CONTENT_LENGTH' in environ:\n        headers_dict['Content-Length'] = environ['CONTENT_LENGTH']\n\n    body = json.dumps(headers_dict, indent=2).encode('utf-8')\n    status = '200 OK'\n    headers = [\n        ('Content-Type', 'application/json'),\n        ('Content-Length', str(len(body)))\n    ]\n    start_response(status, headers)\n    return [body]\n\n\ndef handle_environ(environ, start_response):\n    \"\"\"Return WSGI environ variables as JSON.\"\"\"\n    # Filter to serializable values\n    safe_environ = {}\n    skip_keys = {'wsgi.input', 'wsgi.errors', 'wsgi.file_wrapper'}\n\n    for key, value in environ.items():\n        if key in skip_keys:\n            continue\n        try:\n            # Test if value is JSON serializable\n            json.dumps(value)\n            safe_environ[key] = value\n        except (TypeError, ValueError):\n            safe_environ[key] = str(value)\n\n    body = json.dumps(safe_environ, indent=2).encode('utf-8')\n    status = '200 OK'\n    headers = [\n        ('Content-Type', 'application/json'),\n        ('Content-Length', str(len(body)))\n    ]\n    start_response(status, headers)\n    return [body]\n\n\ndef handle_error(environ, start_response, path):\n    \"\"\"Return specified HTTP error code.\"\"\"\n    try:\n        code = int(path.split('/')[-1])\n    except ValueError:\n        code = 500\n\n    status_messages = {\n        400: 'Bad Request',\n        401: 'Unauthorized',\n        403: 'Forbidden',\n        404: 'Not Found',\n        500: 'Internal Server Error',\n        502: 'Bad Gateway',\n        503: 'Service Unavailable',\n    }\n\n    message = status_messages.get(code, 'Error')\n    status = f'{code} {message}'\n    body = json.dumps({'error': message, 'code': code}).encode('utf-8')\n\n    headers = [\n        ('Content-Type', 'application/json'),\n        ('Content-Length', str(len(body)))\n    ]\n    start_response(status, headers)\n    return [body]\n\n\ndef handle_large(environ, start_response):\n    \"\"\"Return a 1MB response body for testing large responses.\"\"\"\n    # Generate 1MB of data (1024 * 1024 bytes)\n    chunk_size = 1024\n    num_chunks = 1024\n    chunk = b'X' * chunk_size\n\n    status = '200 OK'\n    headers = [\n        ('Content-Type', 'application/octet-stream'),\n        ('Content-Length', str(chunk_size * num_chunks))\n    ]\n    start_response(status, headers)\n\n    # Return as generator for streaming\n    def generate():\n        for _ in range(num_chunks):\n            yield chunk\n\n    return generate()\n\n\ndef handle_json(environ, start_response):\n    \"\"\"Handle JSON POST requests.\"\"\"\n    try:\n        content_length = int(environ.get('CONTENT_LENGTH', 0))\n    except (ValueError, TypeError):\n        content_length = 0\n\n    if content_length > 0:\n        body = environ['wsgi.input'].read(content_length)\n        try:\n            data = json.loads(body.decode('utf-8'))\n            response = {'received': data, 'status': 'ok'}\n        except json.JSONDecodeError:\n            response = {'error': 'Invalid JSON', 'status': 'error'}\n    else:\n        response = {'error': 'No body', 'status': 'error'}\n\n    body = json.dumps(response).encode('utf-8')\n    status = '200 OK'\n    headers = [\n        ('Content-Type', 'application/json'),\n        ('Content-Length', str(len(body)))\n    ]\n    start_response(status, headers)\n    return [body]\n\n\ndef handle_query(environ, start_response):\n    \"\"\"Return query string parameters as JSON.\"\"\"\n    from urllib.parse import parse_qs\n    query_string = environ.get('QUERY_STRING', '')\n    params = parse_qs(query_string)\n\n    # Convert lists to single values where appropriate\n    simple_params = {k: v[0] if len(v) == 1 else v for k, v in params.items()}\n\n    body = json.dumps(simple_params).encode('utf-8')\n    status = '200 OK'\n    headers = [\n        ('Content-Type', 'application/json'),\n        ('Content-Length', str(len(body)))\n    ]\n    start_response(status, headers)\n    return [body]\n\n\ndef handle_not_found(environ, start_response):\n    \"\"\"Handle 404 for unknown paths.\"\"\"\n    body = json.dumps({'error': 'Not Found', 'path': environ.get('PATH_INFO')}).encode('utf-8')\n    status = '404 Not Found'\n    headers = [\n        ('Content-Type', 'application/json'),\n        ('Content-Length', str(len(body)))\n    ]\n    start_response(status, headers)\n    return [body]\n"
  },
  {
    "path": "tests/docker/uwsgi/conftest.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\npytest fixtures for uWSGI Docker integration tests.\n\"\"\"\n\nimport os\nimport subprocess\nimport time\n\nimport pytest\nimport requests\n\n\nCOMPOSE_FILE = os.path.join(os.path.dirname(__file__), 'docker-compose.yml')\nNGINX_URL = 'http://127.0.0.1:8080'\nSTARTUP_TIMEOUT = 60  # seconds\n\n\ndef is_docker_available():\n    \"\"\"Check if Docker is available.\"\"\"\n    try:\n        result = subprocess.run(\n            ['docker', 'info'],\n            capture_output=True,\n            timeout=10\n        )\n        return result.returncode == 0\n    except (subprocess.TimeoutExpired, FileNotFoundError):\n        return False\n\n\ndef is_compose_available():\n    \"\"\"Check if docker compose is available.\"\"\"\n    try:\n        result = subprocess.run(\n            ['docker', 'compose', 'version'],\n            capture_output=True,\n            timeout=10\n        )\n        return result.returncode == 0\n    except (subprocess.TimeoutExpired, FileNotFoundError):\n        return False\n\n\ndocker_available = pytest.mark.skipif(\n    not is_docker_available() or not is_compose_available(),\n    reason=\"Docker or docker compose not available\"\n)\n\n\n@pytest.fixture(scope='session')\ndef docker_services():\n    \"\"\"\n    Start Docker Compose services for the test session.\n\n    This fixture builds and starts the gunicorn and nginx containers,\n    waits for them to be healthy, and tears them down after all tests.\n    \"\"\"\n    if not is_docker_available() or not is_compose_available():\n        pytest.skip(\"Docker or docker compose not available\")\n\n    # Build and start services\n    subprocess.run(\n        ['docker', 'compose', '-f', COMPOSE_FILE, 'build'],\n        check=True,\n        capture_output=True\n    )\n\n    subprocess.run(\n        ['docker', 'compose', '-f', COMPOSE_FILE, 'up', '-d'],\n        check=True,\n        capture_output=True\n    )\n\n    # Wait for services to be healthy\n    start_time = time.time()\n    while time.time() - start_time < STARTUP_TIMEOUT:\n        try:\n            response = requests.get(f'{NGINX_URL}/', timeout=2)\n            if response.status_code == 200:\n                break\n        except requests.RequestException:\n            pass\n        time.sleep(1)\n    else:\n        # Get logs for debugging\n        logs = subprocess.run(\n            ['docker', 'compose', '-f', COMPOSE_FILE, 'logs'],\n            capture_output=True,\n            text=True\n        )\n        subprocess.run(\n            ['docker', 'compose', '-f', COMPOSE_FILE, 'down', '-v'],\n            capture_output=True\n        )\n        pytest.fail(\n            f\"Services did not become healthy within {STARTUP_TIMEOUT}s.\\n\"\n            f\"Logs:\\n{logs.stdout}\\n{logs.stderr}\"\n        )\n\n    yield\n\n    # Teardown\n    subprocess.run(\n        ['docker', 'compose', '-f', COMPOSE_FILE, 'down', '-v'],\n        capture_output=True\n    )\n\n\n@pytest.fixture\ndef nginx_url(docker_services):\n    \"\"\"Return the nginx base URL.\"\"\"\n    return NGINX_URL\n\n\n@pytest.fixture\ndef session(docker_services):\n    \"\"\"Return a requests Session with keep-alive enabled.\"\"\"\n    with requests.Session() as s:\n        # Enable keep-alive\n        s.headers['Connection'] = 'keep-alive'\n        yield s\n"
  },
  {
    "path": "tests/docker/uwsgi/docker-compose.yml",
    "content": "services:\n  gunicorn:\n    build:\n      context: ../../..\n      dockerfile: tests/docker/uwsgi/Dockerfile.gunicorn\n    expose:\n      - \"8000\"\n    healthcheck:\n      test: [\"CMD\", \"python\", \"-c\", \"import socket; s=socket.socket(); s.connect(('localhost', 8000)); s.close()\"]\n      interval: 2s\n      timeout: 5s\n      retries: 10\n      start_period: 5s\n\n  nginx:\n    build:\n      context: .\n      dockerfile: Dockerfile.nginx\n    ports:\n      - \"8080:8080\"\n    depends_on:\n      gunicorn:\n        condition: service_healthy\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:8080/\"]\n      interval: 2s\n      timeout: 5s\n      retries: 10\n      start_period: 5s\n"
  },
  {
    "path": "tests/docker/uwsgi/nginx.conf",
    "content": "worker_processes 1;\n\nevents {\n    worker_connections 1024;\n}\n\nhttp {\n    include       /etc/nginx/mime.types;\n    default_type  application/octet-stream;\n\n    log_format main '$remote_addr - $remote_user [$time_local] \"$request\" '\n                    '$status $body_bytes_sent \"$http_referer\" '\n                    '\"$http_user_agent\"';\n\n    access_log /var/log/nginx/access.log main;\n    error_log /var/log/nginx/error.log debug;\n\n    sendfile on;\n    keepalive_timeout 65;\n\n    upstream gunicorn {\n        server gunicorn:8000;\n    }\n\n    server {\n        listen 8080;\n        server_name localhost;\n\n        # Increase buffer sizes for large headers\n        uwsgi_buffer_size 32k;\n        uwsgi_buffers 8 32k;\n        uwsgi_busy_buffers_size 64k;\n\n        # Read timeout for large responses\n        uwsgi_read_timeout 300s;\n\n        location / {\n            uwsgi_pass gunicorn;\n            include uwsgi_params;\n\n            # Pass additional headers\n            uwsgi_param HTTP_X_FORWARDED_FOR $proxy_add_x_forwarded_for;\n            uwsgi_param HTTP_X_REAL_IP $remote_addr;\n        }\n    }\n}\n"
  },
  {
    "path": "tests/docker/uwsgi/test_uwsgi_integration.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nIntegration tests for gunicorn's uWSGI binary protocol with nginx.\n\nThese tests verify that gunicorn correctly implements the uWSGI binary\nprotocol by running actual requests through nginx's uwsgi_pass directive.\n\"\"\"\n\nimport concurrent.futures\nimport json\n\nimport pytest\nimport requests\n\nfrom conftest import docker_available\n\n\n@docker_available\nclass TestBasicRequests:\n    \"\"\"Test basic HTTP request handling through uWSGI protocol.\"\"\"\n\n    def test_get_root(self, nginx_url):\n        \"\"\"Test basic GET request to root endpoint.\"\"\"\n        response = requests.get(f'{nginx_url}/')\n        assert response.status_code == 200\n        assert b'Hello from gunicorn uWSGI!' in response.content\n\n    def test_get_with_query_string(self, nginx_url):\n        \"\"\"Test GET request with query string parameters.\"\"\"\n        response = requests.get(f'{nginx_url}/query?foo=bar&baz=qux')\n        assert response.status_code == 200\n        data = response.json()\n        assert data['foo'] == 'bar'\n        assert data['baz'] == 'qux'\n\n    def test_post_echo(self, nginx_url):\n        \"\"\"Test POST request with body echo.\"\"\"\n        test_body = b'This is a test body content'\n        response = requests.post(f'{nginx_url}/echo', data=test_body)\n        assert response.status_code == 200\n        assert response.content == test_body\n\n    def test_post_json(self, nginx_url):\n        \"\"\"Test POST request with JSON body.\"\"\"\n        test_data = {'key': 'value', 'number': 42, 'nested': {'a': 1}}\n        response = requests.post(\n            f'{nginx_url}/json',\n            json=test_data,\n            headers={'Content-Type': 'application/json'}\n        )\n        assert response.status_code == 200\n        data = response.json()\n        assert data['status'] == 'ok'\n        assert data['received'] == test_data\n\n    def test_post_large_body(self, nginx_url):\n        \"\"\"Test POST with large request body (100KB).\"\"\"\n        large_body = b'X' * (100 * 1024)\n        response = requests.post(f'{nginx_url}/echo', data=large_body)\n        assert response.status_code == 200\n        assert len(response.content) == len(large_body)\n        assert response.content == large_body\n\n\n@docker_available\nclass TestHeaderPreservation:\n    \"\"\"Test that headers are correctly passed through uWSGI protocol.\"\"\"\n\n    def test_custom_headers(self, nginx_url):\n        \"\"\"Test custom headers are passed to the application.\"\"\"\n        custom_headers = {\n            'X-Custom-Header': 'custom-value',\n            'X-Another-Header': 'another-value'\n        }\n        response = requests.get(f'{nginx_url}/headers', headers=custom_headers)\n        assert response.status_code == 200\n        data = response.json()\n        assert data.get('X-Custom-Header') == 'custom-value'\n        assert data.get('X-Another-Header') == 'another-value'\n\n    def test_host_header(self, nginx_url):\n        \"\"\"Test Host header is passed correctly.\"\"\"\n        response = requests.get(\n            f'{nginx_url}/headers',\n            headers={'Host': 'test.example.com'}\n        )\n        assert response.status_code == 200\n        data = response.json()\n        assert data.get('Host') == 'test.example.com'\n\n    def test_content_type_header(self, nginx_url):\n        \"\"\"Test Content-Type header is passed correctly.\"\"\"\n        response = requests.post(\n            f'{nginx_url}/headers',\n            data='test',\n            headers={'Content-Type': 'application/x-custom-type'}\n        )\n        assert response.status_code == 200\n        data = response.json()\n        assert data.get('Content-Type') == 'application/x-custom-type'\n\n    def test_user_agent_header(self, nginx_url):\n        \"\"\"Test User-Agent header is passed correctly.\"\"\"\n        response = requests.get(\n            f'{nginx_url}/headers',\n            headers={'User-Agent': 'TestAgent/1.0'}\n        )\n        assert response.status_code == 200\n        data = response.json()\n        assert data.get('User-Agent') == 'TestAgent/1.0'\n\n\n@docker_available\nclass TestKeepAlive:\n    \"\"\"Test HTTP keep-alive with multiple requests per connection.\"\"\"\n\n    def test_multiple_requests_same_session(self, session, nginx_url):\n        \"\"\"Test multiple requests using same session/connection.\"\"\"\n        for i in range(5):\n            response = session.get(f'{nginx_url}/')\n            assert response.status_code == 200\n\n    def test_mixed_requests_same_session(self, session, nginx_url):\n        \"\"\"Test mixed GET and POST requests using same session.\"\"\"\n        # GET request\n        response = session.get(f'{nginx_url}/')\n        assert response.status_code == 200\n\n        # POST request\n        response = session.post(f'{nginx_url}/echo', data=b'test')\n        assert response.status_code == 200\n        assert response.content == b'test'\n\n        # Another GET\n        response = session.get(f'{nginx_url}/headers')\n        assert response.status_code == 200\n\n        # JSON POST\n        response = session.post(f'{nginx_url}/json', json={'test': 1})\n        assert response.status_code == 200\n\n\n@docker_available\nclass TestErrorResponses:\n    \"\"\"Test HTTP error responses through uWSGI protocol.\"\"\"\n\n    @pytest.mark.parametrize('code', [400, 401, 403, 404, 500, 502, 503])\n    def test_error_codes(self, nginx_url, code):\n        \"\"\"Test various HTTP error codes are returned correctly.\"\"\"\n        response = requests.get(f'{nginx_url}/error/{code}')\n        assert response.status_code == code\n        data = response.json()\n        assert data['code'] == code\n\n    def test_not_found(self, nginx_url):\n        \"\"\"Test 404 for non-existent path.\"\"\"\n        response = requests.get(f'{nginx_url}/nonexistent/path')\n        assert response.status_code == 404\n        data = response.json()\n        assert data['error'] == 'Not Found'\n        assert data['path'] == '/nonexistent/path'\n\n\n@docker_available\nclass TestEnvironVariables:\n    \"\"\"Test WSGI environ variables are correctly set.\"\"\"\n\n    def test_request_method(self, nginx_url):\n        \"\"\"Test REQUEST_METHOD is set correctly.\"\"\"\n        response = requests.get(f'{nginx_url}/environ')\n        assert response.status_code == 200\n        data = response.json()\n        assert data.get('REQUEST_METHOD') == 'GET'\n\n        response = requests.post(f'{nginx_url}/environ', data='')\n        data = response.json()\n        assert data.get('REQUEST_METHOD') == 'POST'\n\n    def test_path_info(self, nginx_url):\n        \"\"\"Test PATH_INFO is set correctly.\"\"\"\n        response = requests.get(f'{nginx_url}/environ')\n        assert response.status_code == 200\n        data = response.json()\n        assert data.get('PATH_INFO') == '/environ'\n\n    def test_query_string(self, nginx_url):\n        \"\"\"Test QUERY_STRING is set correctly.\"\"\"\n        response = requests.get(f'{nginx_url}/environ?foo=bar&test=123')\n        assert response.status_code == 200\n        data = response.json()\n        assert data.get('QUERY_STRING') == 'foo=bar&test=123'\n\n    def test_server_protocol(self, nginx_url):\n        \"\"\"Test SERVER_PROTOCOL is set.\"\"\"\n        response = requests.get(f'{nginx_url}/environ')\n        assert response.status_code == 200\n        data = response.json()\n        assert 'SERVER_PROTOCOL' in data\n        assert data['SERVER_PROTOCOL'].startswith('HTTP/')\n\n    def test_content_length(self, nginx_url):\n        \"\"\"Test CONTENT_LENGTH is set for POST requests.\"\"\"\n        body = 'test body content'\n        response = requests.post(f'{nginx_url}/environ', data=body)\n        assert response.status_code == 200\n        data = response.json()\n        assert data.get('CONTENT_LENGTH') == str(len(body))\n\n\n@docker_available\nclass TestLargeResponses:\n    \"\"\"Test large response handling through uWSGI protocol.\"\"\"\n\n    def test_1mb_response(self, nginx_url):\n        \"\"\"Test 1MB response body is received correctly.\"\"\"\n        response = requests.get(f'{nginx_url}/large')\n        assert response.status_code == 200\n        assert len(response.content) == 1024 * 1024\n        # Verify content is all 'X' characters\n        assert response.content == b'X' * (1024 * 1024)\n\n    def test_large_response_content_length(self, nginx_url):\n        \"\"\"Test Content-Length header for large response.\"\"\"\n        response = requests.get(f'{nginx_url}/large')\n        assert response.status_code == 200\n        assert response.headers.get('Content-Length') == str(1024 * 1024)\n\n\n@docker_available\nclass TestConcurrency:\n    \"\"\"Test concurrent request handling.\"\"\"\n\n    def test_parallel_requests(self, nginx_url):\n        \"\"\"Test handling multiple parallel requests.\"\"\"\n        num_requests = 20\n\n        def make_request(i):\n            response = requests.get(f'{nginx_url}/query?id={i}')\n            return response.status_code, response.json().get('id')\n\n        with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:\n            futures = [executor.submit(make_request, i) for i in range(num_requests)]\n            results = [f.result() for f in concurrent.futures.as_completed(futures)]\n\n        # All requests should succeed\n        assert all(status == 200 for status, _ in results)\n        # All IDs should be present\n        ids = set(id_val for _, id_val in results)\n        assert ids == set(str(i) for i in range(num_requests))\n\n    def test_parallel_mixed_requests(self, nginx_url):\n        \"\"\"Test parallel GET and POST requests.\"\"\"\n        def get_request():\n            return requests.get(f'{nginx_url}/').status_code\n\n        def post_request(data):\n            response = requests.post(f'{nginx_url}/echo', data=data)\n            return response.status_code, response.content\n\n        with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:\n            get_futures = [executor.submit(get_request) for _ in range(10)]\n            post_futures = [\n                executor.submit(post_request, f'data-{i}'.encode())\n                for i in range(10)\n            ]\n\n            get_results = [f.result() for f in get_futures]\n            post_results = [f.result() for f in post_futures]\n\n        assert all(status == 200 for status in get_results)\n        assert all(status == 200 for status, _ in post_results)\n\n\n@docker_available\nclass TestSpecialCases:\n    \"\"\"Test edge cases and special scenarios.\"\"\"\n\n    def test_empty_body_post(self, nginx_url):\n        \"\"\"Test POST with empty body.\"\"\"\n        response = requests.post(f'{nginx_url}/echo', data=b'')\n        assert response.status_code == 200\n        assert response.content == b''\n\n    def test_binary_body(self, nginx_url):\n        \"\"\"Test POST with binary body containing null bytes.\"\"\"\n        binary_data = bytes(range(256))\n        response = requests.post(f'{nginx_url}/echo', data=binary_data)\n        assert response.status_code == 200\n        assert response.content == binary_data\n\n    def test_unicode_in_query_string(self, nginx_url):\n        \"\"\"Test unicode characters in query string.\"\"\"\n        response = requests.get(f'{nginx_url}/query', params={'name': 'test'})\n        assert response.status_code == 200\n        data = response.json()\n        assert data.get('name') == 'test'\n\n    def test_special_characters_in_path(self, nginx_url):\n        \"\"\"Test handling of special path that triggers 404.\"\"\"\n        # This should return 404 since the path doesn't exist\n        response = requests.get(f'{nginx_url}/path/with/slashes')\n        assert response.status_code == 404\n\n    def test_long_header_value(self, nginx_url):\n        \"\"\"Test handling of long header values.\"\"\"\n        long_value = 'X' * 4096  # 4KB header value\n        response = requests.get(\n            f'{nginx_url}/headers',\n            headers={'X-Long-Header': long_value}\n        )\n        assert response.status_code == 200\n        data = response.json()\n        assert data.get('X-Long-Header') == long_value\n"
  },
  {
    "path": "tests/docker/uwsgi/uwsgi_params",
    "content": "uwsgi_param  QUERY_STRING       $query_string;\nuwsgi_param  REQUEST_METHOD     $request_method;\nuwsgi_param  CONTENT_TYPE       $content_type;\nuwsgi_param  CONTENT_LENGTH     $content_length;\n\nuwsgi_param  REQUEST_URI        $request_uri;\nuwsgi_param  PATH_INFO          $document_uri;\nuwsgi_param  DOCUMENT_ROOT      $document_root;\nuwsgi_param  SERVER_PROTOCOL    $server_protocol;\nuwsgi_param  REQUEST_SCHEME     $scheme;\nuwsgi_param  HTTPS              $https if_not_empty;\n\nuwsgi_param  REMOTE_ADDR        $remote_addr;\nuwsgi_param  REMOTE_PORT        $remote_port;\nuwsgi_param  SERVER_PORT        $server_port;\nuwsgi_param  SERVER_NAME        $server_name;\n"
  },
  {
    "path": "tests/requests/invalid/001.http",
    "content": "GET /foo/bar HTTP/1.0\\r\\n\nbaz"
  },
  {
    "path": "tests/requests/invalid/001.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import NoMoreData\nrequest = NoMoreData"
  },
  {
    "path": "tests/requests/invalid/002.http",
    "content": "GET HTTP/1.1\\r\\n\n\\r\\n"
  },
  {
    "path": "tests/requests/invalid/002.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import InvalidRequestLine\nrequest = InvalidRequestLine\n"
  },
  {
    "path": "tests/requests/invalid/003.http",
    "content": "GET\\n/\\nHTTP/1.1\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/003.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import InvalidRequestLine\nrequest = InvalidRequestLine\n"
  },
  {
    "path": "tests/requests/invalid/003b.http",
    "content": "bla:rgh /foo HTTP/1.1\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/003b.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import InvalidRequestMethod\nrequest = InvalidRequestMethod"
  },
  {
    "path": "tests/requests/invalid/003c.http",
    "content": "-bl /foo HTTP/1.1\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/003c.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import InvalidRequestMethod\nrequest = InvalidRequestMethod\n"
  },
  {
    "path": "tests/requests/invalid/004.http",
    "content": "GET /foo FTP/1.1\\r\\n\n\\r\\n"
  },
  {
    "path": "tests/requests/invalid/004.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import InvalidHTTPVersion\nrequest = InvalidHTTPVersion"
  },
  {
    "path": "tests/requests/invalid/005.http",
    "content": "GET /foo HTTP/1.1\\r\\n\nba\\0z: bar\\r\\n\n\\r\\n"
  },
  {
    "path": "tests/requests/invalid/005.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import InvalidHeaderName\nrequest = InvalidHeaderName"
  },
  {
    "path": "tests/requests/invalid/006.http",
    "content": "PUT /q=08aP8931Ltyl9nqyJvjMaRCOgDV3uONtAdHABjoZUG6KAP6h3Vh97O3GJjjovXYgNdrhxc7TriXoAmeehZMJx88EyhcPXO0f09Nvd128SZnxZ2r5jFDELkn26reKRysODSLBZLfjU3vxLzLXKWeFOFJKcZYRH9V7hC98DDS4ZsS7weUksBuK6m86aLNHHHB0Xbyxv1TiDbOWYIzKxV0eZKyk0CaDLDiR0CRuMOf4rwBeuHoMrumzafrFI5iL72ANQZmOvKdk1qQeXkRqEG11YU0kF7f1hSlmgiIgg5maWiBsA9sAg36IIXZMWwJF63zpMgAyjTT8l4pQhSBfhY2xbGAWmLGpyd1rlBm0O5LCoKpnQuTACm2azi0x6a1Qbry9flQBO4jHge2dXiD1si6Gh5q8fZu8ZQ7LLWii2u4rGB7E4XlhnClrCHg5vJmjYf2AItYPA0ogsiIdEEQGpzMJPqrp8Icn5kAAimWF1aCYaDjcdSgWI48PnoxlzIHX50EPFcPOSLecjkstD9z66H554sUXfWn3Mk9lnOUlse6nx0u1YClFK4UFXp98ru9eBBr7pkAsfZ34yPskayGyXPPyzWyBfVd28UuvdEG47SMdyqEpX0rFdk67fAYij0PWMK79mDmGAS37O821o18XUbu0GQjsqAGVMN9LDIAliD9QqtlwdEnplKkUyyZ7GAFJCFffgzppU9CjA2FbPX6ZjTOi4sPoYEyhyeQKVqAe9keYeDpU2qDwq83XEDQUKvP0w48GyavSmdBcrMXjUsu0PfdYpSaKwarrUB3i93HgoQB3ZJIR4lW6iPRTmm28OEKq2MIJGAoTXxCZYM5UacRldlqQOj6JkYz6y7ppWOjJ9yiCUEenuvfcItgmw9HIgGA59JxO8NDLEZLSONfuIgiV7wjsJnxuTOlU4vkjV7fTuOeU91xez7UKhaTqqEW3XBUSLjhKi3IkZg7ukrGZTWPhijFv2EZwEWDAyLlHvZB4X738zGJUlEX1k52EHwrKVKdLfePcaOjAGKsongHBFYxYC8vBBLuKm9RWexKCT14M25pCGloJXZ4OpBRfDQA2kobLUcEXEpzqRBPGN2JdNSBOFlUtUxWKnnPBM6r9S356l3k1o9zTIPeoIitWRjASs4A0iwYc8p5vv5Kt8KtsmW7Xv8dlU8HbZHsy3LI7O9BpUH8cJubqdEhooKABkx71pdcsZGhZb6epyTiPyvOhdJ7tNtFy3KQOameqTgGyd53Z42eZ0AjaOEvnzermi2E0xo3MMHFhB74TFtNAI3ppxxyqknc1mzUqZ49Wi8YPBg9ids6IgZvddBQYvwEozkmyGAkatQtt9TD4LjU3TyyUlhNG21q7CzEEl8NNsVrV6QyHsfw7E5w7XcoT7OQkBYoZwHIAjfekehnpc2llRtRY5m43fPVasmsVazOR36DRSLZJPHAqUDO0LInu9mgP57Mnz9CgylEmdE2aaYs426rnTFR3G3CfjLofHfjaLOkAegr4W3jx6MNMMOMZw2u46YTCnlfbBK6ZA1UYeAH1DIQJykcSQESinC8HpYIJt9A8g7UT0awzRP1F9nHa3wDnaAHndQYKMrjzlWo8ejQ0XHWgHhqnWHgW4h9sOnJckH00CYK1fHUKASJ3D8kOKax6uplexfz6BCvAoL9zm5TjeB1yxrpLp9NjjTWSKG2HOZhPkGpdEqU4mjnN2AkUVACPGos5YLBmTnSrdOEGZJDlAvJOUt800Mu3BYc1MiDIB6LMSSV5RsIUDFOzNletGQoq4G3yHZmx78uEse5vUTPFF3KT8LCrssqdIU9H97Npgf6N5j8arQ7ykLzN459jJaUzpGIo6uowPnUSatDf9GAvAmWNvsVTz6bYiAV71C7QF0C7UolYIQY6DHJEHejgX2YMEovWNLPL50eeC51h4DdPNv5G4ZdNtQTRVybYBZMpetGDiFmXN0JKa1sKHOSZxdrhKjxDIhrYVyCcRUMQ0sjGGHFuOcRszr6E5igEMtsebHQ3KYiGd5B27LikpUHhk61rgZlulHdMoS6YgQs6SV6UMVNku6sCw529xhUciDwRMhsbAjDlahYbrGa3NryxyV5LrXONGGKCchCqv7vDMdAtPrVr8M2vL5MySQAC3g90iugGQcLH3hCf9f1Kn5X0hM4KZTfwOPJhlfJsMRNhssiDoXaycUvOUS58266yPDlitPIAzO03XClm4EDPXGIwcwiFr7FcDo3tQIMZVy87i48Zb80s3zAYRiBIS0vO3RKGx3OGN5zid2B7MfnfLzvpvgZoirHhAqXffnym5abpZNzGuo5GowTRA2Ptk4Ve2JFoHACWpD6HiGnRZ9QVOmPICoQrSUQw45Jlk9onKJz5Erhnx0943Uno6tMJ5jbrWBNiIO7i04xzRBgujeiAJvuQkVDX2QLKRxZ7s6rhdfOaq6R6uL108gEzzlXOLqTTJXgM63rcUWNbE7wsIXcCFSF59LLJ7G5Qea33suxdDX6DcK4a0VMZoxmWPtCi1dAT9ggJqc2Sh7mkAqizaB16RXZvSydchpdVj6s4qn4ivr0HKHdAstX0XZ0FFU6lOiNmU3vasMg2uaVG8tyuG8N8VsuXIOQs7xtFxDhilYb8MQ9vES9pWfWPSXFlJAq4XKPY8a0JOIx57EQuWHo3uWgRTIRThvZP9YYzSnjGIHwjS8JeppICHofADXZhJ0uDQaQs7MiXEALpGmT3W6w0G3tBdZcuTDkWx1HsT5jd9jQeJpgD2VxdKh8U4Q3vANTAuwBXLJ2P0stS8Q72JWgNPwKYTY9cPoaGZlUFGgVsq8CdEFH9yW0c27G5s5sfHsyep6t4VxIHHMOX2GmMRyGxDI33am1J7ZmJ1NyXiwkHxtPH5QBpU2PMu2Guf3xIxlk3snMkMAsGO0vYfqO9tdIgdxMYO3HZTYv99OXaHcNQ5u0pRZZyVrNOIPurkEOdJy0nowPemIgUuHWh8vQCuDZav1m35AOl6ftSFuChSm5KstEWnC7q8mJ0juJEBkCRmQphP3V1pqiDjz6YA90qEe7MA3nzT0nHG8A1hWlqcPVPNz4qWNF6Fq1ub4075aXO0H7Krb6rhWGb3ZRPjpb4BKN8jGFQrBUMZprtjAJ67BnfmYgE0mmGLV2QP10gYS1T06kBRyrtp7he6wsPiBPJ7wxPLHNUN2SGQHBTSKagndM99fuaga5Sw9OT8Fzdo7xUJXfhJ97gUnNDrknal0B00NMNvajZeQQTJyBsVSwBZtZ45ZCcq1idc7GWC0MITSk58cIVkSPXbrERUaygyY13dPeEVzjVi9aVJwUF6eJu1s8u3FCJqp2GoWIItwvZO69asX75fekFkmFpNavxM0X0dZC01TTPpV6E6PJoIfW8C06CKNHV7Gk2mkTWGSwUG4xD2L3G3XarodHDcmumFJX9Xviv0rvm38SCtin6OpjH8MHYDrj1OxTJbC2VclJxv73z2BDBquosKOik0fmgbPZN0FUTmjBEwHTvqd5QHTwb3nOpEz3X6YCF0lrcrQc0uhyr7gBGBs86nUBWFRp1LKjIRVTVXDipajqNDTQGNZtzvR9MUf1yJJV07inbrlPOENd7rHpKCrJtoZXOkDqInaIqoMCG3DVd353BGmZNJEKOa3DnL7fb9zwuHlvHAfCco7ZS4wAV87trWkp6skXux9v5WhkumbUyGq4ia6DM1PuqqnFfBTAWDzJsnggAJrzr8O7JbDtaXwcW9sqaOb0S6NvnUDZqiNdDQPMDOKvXRJJJQdf1FSrPCCSPEEWO1SeVwictj7rTbpWGRoukwhgJALys95pGGOQxCPzRGrtVFnGcsLN1CwI3wLbmDnNKUv3KpOLEOPRxQXeXuJRIiYCFum44c0wNr731DvHn3YEJMH4iwFONl1rolEL4w6KFUOCq7ekrE5iyUt1V32PNtuUshXRjOYjBval29JMH5GoqZlGhCczzHMA61cmuzqdFwiPCB9yzqvJTg8TqMNvwKJztFIQK4mc5Ev5rRVSozD796AVRKT8rZF39IA1kmCLdXqz7CCC8x4QjjDpxjKCXP5HkWf9mp2FNBjE3pAeaEc6Vk2ENLlW8WVCe HTTP/1.0\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/006.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import LimitRequestLine\nrequest = LimitRequestLine\n"
  },
  {
    "path": "tests/requests/invalid/007.http",
    "content": "PUT /stuff/here?foo=bar HTTP/1.0\\r\\n\nServer: http://127.0.0.1:5984\\r\\n\nContent-Type: application/json\\r\\n\nContent-Length: 14\\r\\n\nSomeheader:\n0X0VfvRJPKiUBYDUS0Vbdm9Rv6pQ1giLdvXeG1SbOwwEjzKceTxd5RKlt9KHVdQkZPqnZ3jLsuj67otzLqX0Q1dY1EsBI1InsyGc2Dxdr5o7W5DsBGYV0SDMyta3V9bmBJXJQ6g8R9qPtNrED4eIPvVmFY7aokhFb4TILl5UnL8qI6qqiyniYDaPVMxDlZaoCNkDbukO34fOUJD6ZN541qmjWEq1rvtAYDI77mkzWSx5zOkYd62RFmY7YKrQC5gtIVq8SBLp09Ao53S3895ABRcxjrg99lfbgLQFYwbM4FQ6ab1Ll2uybZyEU8MHPt5Czst0cRsoG819SBphxygWcCNwB93KGLi1K9eiCuAgx6Ove165KObLrvfA1rDI5hiv83Gql0UohgKtHeRmtqM0McnCO1VWAnFxpi1hxIAlBrR4w35EcaryGEKKcL34QyzD1zlF4mkQkr1EAOTgIMKoLipGUgykz7UFN1cCuWyo3CkdZvukBS3IGtEfxFuFCcnp70WTIjZxXxU4owMbWW1ER5Gsx0ilET0mzekZL0ngCikNP2BRQikRdlVBQ3eiLzDjq27UAm7ufQ9MJla8Yxd6Ea37un9DMltQwGmnmeG5pET54STq72qfY4HCerWHbCX1qwHTErMfEfIWcYldDfytUTOj7NcWRga3xW7JYpPZHdlkb24evup3lI4arY6j5a12ZcX9zVI02IJG0QD9T4zSHEV0pdVFZ8xwOlSWKuZ9VZMmRyOwmfhIPA7fDV5SP8weRlSnSCSN4YBAfzFVNfPTyeoSfVpXsxIABhXEQTg12YvAAn9390wFhEhMsT9FWIiIs7oH63tQyjdEAZSJcZ0nSQfapvi4BDsQSMv3W2DofSzxwOPrVQWRMyvP0UV0J660Gc4iZ2Tixe3DSeqg9VuNvij09aCbkBdwJh9r4UWmM1Hp1ZDF5Rr14nKtFAgjVlGlfZi4bWQKTzOlqaVbWBvxdKsJ27eelyDnasIPqo17yY5lg10Lb8nyu60Wn7l7Xb0Ndp334B5am4Vh1foctvkkhNFeIejtnjPYmWjS77rJ1aL0zJka4Xog5Oparvc93Pddf9CzCxgle00BTKNj0syVo5uqvX5PVzdhAnigU4jdPbJbcPpbpJRU4UDqIswRNJOlGfpdLnCvnPIRB2a7btjFTaE0tne0TjedGbePje1Li21rPXPX7t5LICWl1SRyqQ9x9woGEv1sI5VgpRoKtS6oxWgMERjP3LcEez3XqLiSwv0rWMlDiJhxEopz8Mklx8ZygQLiwIYx2pNq0JhKB8K1lZ8dYE5d3nRWhXwG4gFTUg2JYjnjL81WGRmjXnZEVLwYfYBUkRlqWAYHi1E6wF85BfcwvkgnEeBTiQSlfu6xwCYaW2OEogq7tbdinvlpeEPij1qQivpcs573HPHpkXrEeXC9P2gZhmV1Rvn69NAN2lOXSVe8XotSyCG5fHFsTDYlOvYW8EBrAdWuZrwU753xwjk3QCp2ODetYze98voig4lfYHrrWT43VXcHt8J5z7U3kt5O460buwESBhgkALZdrFYyy4YQcmnAeSCw5OoLArDEmzaI4JkFBCDqQxTE9BTYA112r9ymuOo5MGkTDYZlvtvopG4ekorfLoIa13Z9L6ZilXT1cg55dvNlOrbTSHpQTYRJfJ6x71IpDFyvdbZbOHQYMm98fcN9CLqFErkpcN4JO26GIhSodGGTSnzyUxBYueawFNlGxCMTa6JseX9c7Xlo8NRaZHBPvG7Z4gUCkOdUSEW0RRTs3TSSdjEKnJ6u9RdDqqyvN8cJ7gliTd04mSyVnkmxdqVU8DrdIrkSCfVQNoFgdydDHS3wMLU6QGTGBzK5pd9EfsDEeYXtIb3CkRupM4SERGMTN8TyIxqqIyWmgjBmSGLTFOB5tsPhkVydVQNf7jBkDy6THfBy0uALVUkm2jLeTFXjajyeL4ms5Lgx0eLoz0XWN6WulXSA20zV3ObSCHbBeVUgKmPxHq5qPmAi04VFIvCOJ0rBQJh9ZHJMwvhI3VEBF6EmXOiRCn0XOhm3pfHlmaCAWrOSGuQs3NCNlFRjwmVRPY5FJrKYjH3FrLrLdU07zdViAix8C4LxVrRrMB6ligZC3CoDhFA4vMjiPU5SBRqRW4lwVnvMZEZbf0AYbBc2ymnKAOWbQwt2ldiI2qL0aLoL6YtSFUhpwMOR3LP1feUq6XRO5xc9V02nEt9MRQsl5MgmKMcXap4HqAN0yATpjAGRnWqEnE7E1XZg95cEl2gO4HXejKzR0kiTUudcw6P4t1RYLRx7isZNJxiq1JZz6FpEe7QhwGbhPySNMbXJtmYuhAaTpfGdGKMxvHHB9LmELOChdyfjHMwMZ2B0xgU2eJgJimCwLH3UEmExgAwJDD4GSCqevYAMK4P9FKPl0dku0KZ7uOJ8oNloEsrbvMuhuKFDuO1PNvxtdCcgASzNVzdueOtUm1giZIDqbb6j11nqi9NoFeck1zZi2kfGF7OeUp4vYszuhQNi4vd03QeVAduM9h9v36Nz1YobRxB2CjTp6qdKdW9IYBp8aExZpipnJIbfD2hTWE44kIu7Q17f4C9kycGjsLwAWkVbfTRmBMU8SbVKV1EJTrN1gGqGX7quSwg1Vp4qslKAk6EIkoReIl5DuzuH8Rbvrkp5LFFAhNhb1hvXvVWcibtDjQSradNtuYzGf2AAduhxOTnZjzbsceGYhQA5a5NtqxE2GBlW8CPoPzIyfMfPjdAIUmAcns7Fkp44nju2htwhryUyidEzDVyTwevquARjt5a7eu8qIKfPrYgbOAlPgA1JHNi55ivTNpDuQ8drNiafZIntA43HI447WtITYYvLxFRG8OWvJRwI0N7dvHYO8H8lYI1OwatfvLKlJqjtdJBBvMWXdT4SbxHUdNTDUQmqFGZaLx1AvYPnJTYRzrqn5ZnXyWQ1ZCwtvZK209TxoezJ2sGorE46C7Zyki6EcXlX2A8upUUh9IhqLYTzidIRrAPE5mZmosyDyShjnRiN5CLXZAI21eV4v3a6WXI8TKkUk3fhhajOgPXshlyCEfDAyESpz1J8RECu6vQs81E1ZNE5ha5UGw2wk3Ea8oSTfqTiu0OeisV2a6bfldvW4x0OL8PS57uuY0v0OZPSUPWmPQgnmJRVw8vmh62bpFekMnUH7y31fXU6MIyZaiBs1FEu7qF6irBszHt2ARy50SjgGwQZWcecgvB8gB874g3ES9mZer3diYGF3Wssmsm6XRdsNcuNn3yzuoi52cRrBYUOISegTBVApn4zfuCC9Y4AAfe6wmmiuN8hL6KJeOjrdK5EFQHGyrzeuIMaT3B2nKz1PNONVQ0udbqCQebz3cq7NPe6kGKFLiE6euWjdoMuAbuu8rTkAa42ensXz4a1Yo450ZVgYypaDtepDQWFkJyTHDW1HTVZfCok0tp7STRiQ8n3NKxOUSL9veuTsDs1FaV2rbzR3DvkEJrhJ10Rm0pvLgui5GUDKyWLnrqcNVtOIzFaj9K5pwMfnREm1VIs84ePX0GsMjirfOfubzDoYjavbiCtTB86nKx0tfCKtl0yUQ5PWSBqdGASY3mr5hZcFZ9bA6uXXGTNqMpUH3gqxCoF6t2yAim93t77jYkiFt3OBlBRVQzRsPbgEKRXbX3bWQj6NpDzNCQPYTs45HsQB967f4yByzLH8X289YAZJhJJyFTMCLbpdKFuMBX5Msyr4d15sBa1h5bI13dqU14WBnMKD12LkHMjHiyde6xf5EELf082sUfiAZaROFuDCDnA89p6y6oYEUgF1L9yQElZO4R6IrkJsEFN9hvARf3CH4ENqbYxtUN9gsB9CLCGKMy2R4wGKU3Dkyea27YCR4QHCdqX3HqOpy12uxBANvbrfEro9q5NJrGK7WVq3nNabN05x4TmIZk3asc8ehvDyhSgQLY0wwyvrkcYqNiETybJ57RjwVg1YE0IZEBfyAUNXE4goc2jtbZbHfcpTzt08pSJQZTAzuxrdQLS4EnaFHPpMdPh1YXUdclj6g2sjYbhoTYcV97bVDAUztMZ4EarUcv6tgQOvK66RmJCF2zVEpFDBS6AVZJWzrVlnuiweXpH0L9eY2Wy2EuAHi7gL4o0i0AkOapqY1TPUWUwBaVrKQzkL8QQbczgc97pMvSnGYMlcSdzlamFtUmRoOPmhBGMpVqmcxnstnqJ0TXMV65zbRN2hk3YVF5HwPjuWJmfkVYnyazuqKuaaohrQIe7YOOSAmD7C2vDnI50y1oScQqIPb87QAmguFz7jfNBSPymjPJ7UrToaJen7LEQr8S2b69ayZYNIyWbcpaW5ACUqdyT5AeHYhdENORnWS2B17qnBPtyvb4WujJCafLmsMFhQbcGonDZkHEOAnOcwRwJ4KIPr4MlQLRKsdnurPDDEmpCtCnFg8vPObOPHoHgICb9j35pG1YNhAAGIGTZ4g3JTJzFvTcW7GDRxREPZffKOuQTJoMYYaaPwnE0SainEpCFAukJbDy1ss5cZt60nqTw1asLzwMKJu5PHpU9sB9YN7J2cPhIbfb4387zSmSvqbt3I8NFjDbuYEhe6nZ7gRT5Th0W0MoyzHlmy4MSXbaAfUJNsLQJmdhdVKDsqMz0aXKIVNsXtn88owrhw0yqxU0K3IfTothafhpQ8daRUnbjzULViWRvUz7dI1N3GgylRzaEXQPgbj0DQ7RujNTcJoSp7I1ELjFFSBZDm4Jx5eXq0aS2SKJPFX7XmFfkkR99wRiHx4ByVTL5umojRhY5j8vg3l3yfliJbeOTXckaYiezrucuHaiVFWR2kjk9PUm57bDpvtSFMic652iDufj4hqpy5MH5r2lg67T6Bbb3fcq49cVJ3hkN2GfRqVhoPxmHyvotu5koheVh7oHDaLaf4VvcQMd5MF8sicaX3GXfoLjlfFZwfJBpXNbbVemD7XghpIEwuFjA1USU8yJnTdvCJ2bFmPNWFeWsBVDyl7XUsbgB3K2zz806xODZT639dqiqhGXQNbgYtShikQhiHhZF4wf4IY588LE4EO2bdXBb2Wezm8Gl2J5GAfqnx5Z6NF7h1gGkM27hpnmKNylKZjqTNANj0CRU4awpdVrYGX7hT0u452Y5bXpVl7cLuK7j2k7VG93NXPsXADhQA8R9WDcpU0PLzFWFq1omoQ9ZRSlvh8R4pRp4vHIYf4A5uQEmv5Owr4pFQcWdp5GAdkpBaSHvUhvMxOSpsqVB2LHvvs1RiOUHHhHdZEKpX25mK9moud8pKT4efru1SlRRSsxdz87hTJMUrueydHDPXbo9AvExctdqxuCk03Fy8cB57qrkQQ50oGNuTNPColMrwVfmuTt81uSZremLbINILnCVXEnvTugRQfFYMnprqMB4mVJfZfh6XVLdOyW4BPaFrBsZGFy7udoWJwE8ACx4UpJW6m1ltckofzA6AUxzXprXDCCL118m8bBB2hzDKmqeLk5ZYKsLROkTqRAxmJjBSZSo2XBroO5rVvkOZrOZRe8NgaHFMLPn0I6hsqwA7VdKlpbqknax84iWrtBe8ErxgPIQeYhELyK1deW1YWBagD21MBTc2h5LliIlglZg41H8Zl3GvUv0XNZegR5bx1kiM9WFGV9Yt37iQQGquWAMKCAb6AqpkCtKs7sXKaEAVsbh32tlkAg4ngspjwzYHTPYKUuigPX5K8siUfaAW9WJl7r8dc4ju97osWETOcBENLsfwB66TvsttORtOedylnErplZP3hjt7o39JllXDobj3l10bSr4B09eYVWi2DLGavYktKSKj1PrqzuGUaqcFxqoebpuDEAx5vl8ZmSYrmS2RBJ1n2s3lkKdaVWTmfIXlyMMT7Ac3lCXpGNnpf8ccTffv3E0fBrpCSpVc48dM5e5iTpRPrfWxAjrud9jSrqVBXsw3pqUvhuVmBpmwoKAfQGxHrauna3f48AFefGDozxXXjpdM9ZDWHsRUBTFNzDs8tUATtegSzZfNJCS9k0p5q2cueyU1mtwMJIdf0FrsVGiAyX7PFkWvLHi29fpprZQd0gbMMw2Bt10ZbZCsjPX261cXmVa6ZPnkVQm2w1ory3uWejuq20oQCyXTYyv1Ki4tbdPxoNn04Je7uS3QHDCsUl4i9zKNhBJ3g55bhIZWfwmLi3S7oY16gImdC6vvjsMKkCPzXv4pPaVhHH7o4f0mWEz30k4o7GQNOUy8LPM3NmlZF7QaIBdRfozG86jwQkC3jTNR357pdPjOqMERtIS4WEJBgbaeUCu5MOhsNdaD91iCeghIpOECFyTdEkUCGPPCIAtuAOKBdhPu40UxHx30dELMTK3azHOuOnLTsdiM4KJ9yF4Ab2eiz5j2T95sDx3aiEJDVDPCa55hO0XTBM9OSNtdzjdTdZT19XrwD0wPWZcBhfJ66X1uNM2eud1btzglqZP52qqYU7BK2M3BBZKKjy7P6YzmgaPHWnFGHZdwdz3Yq6e3N76Cjkfl8Sy0mkwd6pt0geDM1jNNZrcT8dUfLLaiUqcZm1KRVdpZaBrboDSuCxfWYlxqgsldwlGL4C06ceFUDXX8PzxzWEgOd8OU4F22pcNJOnwJGo6rYA3tvhAuq2WKVg6tgFCb1p7dzF4Ke3J0dv3IneMSNnHG4hkvxW6VzIykDUtYEjMQO35tdnEA0vMVLXIahpJpz4HGs5wwRgoZx1e1zD1pXi7KmEVTlfattgcGFlKjZJ60fEdloZEmiXodxT63CzuJHnjHDOL8qcMzTxHb8OCainga4w1fk4uILLAWqmTFpDcFGSF5lbOFUwhvtMK6knIWZ8ZApZvTGBt1qv3xKUJqPcWiweI4kk57zgyTPZku2mg4fJWDKSfiRSi7LvtpKkdqjein9lP7LMv5lKutprVzjmvHBPjunXGqakWx39xYH8RD6qF3Fw2BnIIesiicZsDv69Ggbu9Y334UeFPNIJ3LGp2I8xcUxlP5dJAh4V05p1HvIZ5Fhk0oCWlvNXdLqzbVsbfW9jWyQTaZXzw7WT3rqFQc7wvw4ayp5eKmUclqB1yOvrI14XGhmH7QMaAYNTIE2RHjYXVgvbmFRi0oB1v4nDEeSTn3KHBRQD8TilCagKg0XYPj2eAgWs12ZRYzlGyCvYZ1pol5wAwc9AFFGwsTJ9UYkbxlZv7wKDx7nFzlUSMC1kMvS2ECwvHzSycqHPRwCGipvG6kWz0mGvASXeKjm47iMROoY0MRK0uvgNdTTOTdxkMgOuCDIlxfit5QKjyzaVAg2kDwENfSd6XPMgSprTSLuNDXdg5NHCwUvDbEHVxpMgOItZymPZtPweOrnPdlEB4UwLZ8jqtShi5oDYvhkh85FwwT25OHFvDUWTTCV5n73pQ8kLo8zsB3mbWfGwg62guj3C50Dh42fAZEPBRSHDRTg3r0z39Vyj490lk2UpZeNyylwuEKmuIqEkbE3BRT2YEjTM8a2PU5grCuzculibcoRUpb1sIQiMRTf4wrtT1CnKcoUJ1T28DC04dTJVRcm3w3WzNLdrnovkX6NahblTzDvq5eXkoEaZv6HClmGuho4FH6s6i0OdmmW8qkNOnk7BhexiyAd3UYERlFwvZ6LP55tFOc3vnlhyylx1rTTgu1NFljRNs7rGiT7SnGFaFK7GITEZFEYI7DmOEUZXxDSHjYuOVN0YAJP2cZFgagyMwGJdrpH8S7cewYPMKz2Go2GBKl1OA6pJ8T91tUdEcGVg9JCMQUA4sBtlIuRTVV3cduIhsLCTi2ewItkh9MRP1kevVa9WcXejQQKreZmq5EZtzThW71r7E2tcvwFeqiwv3JZnV16bZ7NwZT6uvSrOnIFUyMsxhh8xCkVY82VLTAZhPXB8t6CbyjZ5stos6WmNZgoEsD8GU8pmzSTubAqQXkTbiODF2pePe6S9uQ9HngGGBnOjY4QUcAcScDsfflyXVqyxgTelGD4vXoba6qRWCqc9LKpyk4jCKYvLX9tzXusO7bhT2KRvF4MObDqdE4KnCCIF3zeVD0vImR20MmRTBHRCNm3s6GfyeTYEAlW3L2igZJ7Myj5zGLccMt2EohGc38HfWZ4mlvXRLHKB233PyKALYifqlAxTXaWUk13o6nACQDvN7DxSCA0daJeuznK1Dr52bC4IXCTahK1An6LkQMfsXb7Qus6ey241Vb4wTgFHqsdCx7qPxeAghmsTOHRVl\\r\\n\n\\r\\n\n{\"nom\": \"nom\"}\n"
  },
  {
    "path": "tests/requests/invalid/007.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import LimitRequestHeaders\nrequest = LimitRequestHeaders\n"
  },
  {
    "path": "tests/requests/invalid/008.http",
    "content": "PUT /stuff/here?foo=bar HTTP/1.0\\r\\n\nServer: http://127.0.0.1:5984\\r\\n\nContent-Type: application/json\\r\\n\nSomeheader: 08aP8931Ltyl9nqyJvjMaRCOgDV3uONtAdHABjoZUG6KAP6h3Vh97O3GJjjovXYgNdrhxc7TriXoAmeehZMJx88EyhcPXO0f09Nvd128SZnxZ2r5jFDELkn26reKRysODSLBZLfjU3vxLzLXKWeFOFJKcZYRH9V7hC98DDS4ZsS7weUksBuK6m86aLNHHHB0Xbyxv1TiDbOWYIzKxV0eZKyk0CaDLDiR0CRuMOf4rwBeuHoMrumzafrFI5iL72ANQZmOvKdk1qQeXkRqEG11YU0kF7f1hSlmgiIgg5maWiBsA9sAg36IIXZMWwJF63zpMgAyjTT8l4pQhSBfhY2xbGAWmLGpyd1rlBm0O5LCoKpnQuTACm2azi0x6a1Qbry9flQBO4jHge2dXiD1si6Gh5q8fZu8ZQ7LLWii2u4rGB7E4XlhnClrCHg5vJmjYf2AItYPA0ogsiIdEEQGpzMJPqrp8Icn5kAAimWF1aCYaDjcdSgWI48PnoxlzIHX50EPFcPOSLecjkstD9z66H554sUXfWn3Mk9lnOUlse6nx0u1YClFK4UFXp98ru9eBBr7pkAsfZ34yPskayGyXPPyzWyBfVd28UuvdEG47SMdyqEpX0rFdk67fAYij0PWMK79mDmGAS37O821o18XUbu0GQjsqAGVMN9LDIAliD9QqtlwdEnplKkUyyZ7GAFJCFffgzppU9CjA2FbPX6ZjTOi4sPoYEyhyeQKVqAe9keYeDpU2qDwq83XEDQUKvP0w48GyavSmdBcrMXjUsu0PfdYpSaKwarrUB3i93HgoQB3ZJIR4lW6iPRTmm28OEKq2MIJGAoTXxCZYM5UacRldlqQOj6JkYz6y7ppWOjJ9yiCUEenuvfcItgmw9HIgGA59JxO8NDLEZLSONfuIgiV7wjsJnxuTOlU4vkjV7fTuOeU91xez7UKhaTqqEW3XBUSLjhKi3IkZg7ukrGZTWPhijFv2EZwEWDAyLlHvZB4X738zGJUlEX1k52EHwrKVKdLfePcaOjAGKsongHBFYxYC8vBBLuKm9RWexKCT14M25pCGloJXZ4OpBRfDQA2kobLUcEXEpzqRBPGN2JdNSBOFlUtUxWKnnPBM6r9S356l3k1o9zTIPeoIitWRjASs4A0iwYc8p5vv5Kt8KtsmW7Xv8dlU8HbZHsy3LI7O9BpUH8cJubqdEhooKABkx71pdcsZGhZb6epyTiPyvOhdJ7tNtFy3KQOameqTgGyd53Z42eZ0AjaOEvnzermi2E0xo3MMHFhB74TFtNAI3ppxxyqknc1mzUqZ49Wi8YPBg9ids6IgZvddBQYvwEozkmyGAkatQtt9TD4LjU3TyyUlhNG21q7CzEEl8NNsVrV6QyHsfw7E5w7XcoT7OQkBYoZwHIAjfekehnpc2llRtRY5m43fPVasmsVazOR36DRSLZJPHAqUDO0LInu9mgP57Mnz9CgylEmdE2aaYs426rnTFR3G3CfjLofHfjaLOkAegr4W3jx6MNMMOMZw2u46YTCnlfbBK6ZA1UYeAH1DIQJykcSQESinC8HpYIJt9A8g7UT0awzRP1F9nHa3wDnaAHndQYKMrjzlWo8ejQ0XHWgHhqnWHgW4h9sOnJckH00CYK1fHUKASJ3D8kOKax6uplexfz6BCvAoL9zm5TjeB1yxrpLp9NjjTWSKG2HOZhPkGpdEqU4mjnN2AkUVACPGos5YLBmTnSrdOEGZJDlAvJOUt800Mu3BYc1MiDIB6LMSSV5RsIUDFOzNletGQoq4G3yHZmx78uEse5vUTPFF3KT8LCrssqdIU9H97Npgf6N5j8arQ7ykLzN459jJaUzpGIo6uowPnUSatDf9GAvAmWNvsVTz6bYiAV71C7QF0C7UolYIQY6DHJEHejgX2YMEovWNLPL50eeC51h4DdPNv5G4ZdNtQTRVybYBZMpetGDiFmXN0JKa1sKHOSZxdrhKjxDIhrYVyCcRUMQ0sjGGHFuOcRszr6E5igEMtsebHQ3KYiGd5B27LikpUHhk61rgZlulHdMoS6YgQs6SV6UMVNku6sCw529xhUciDwRMhsbAjDlahYbrGa3NryxyV5LrXONGGKCchCqv7vDMdAtPrVr8M2vL5MySQAC3g90iugGQcLH3hCf9f1Kn5X0hM4KZTfwOPJhlfJsMRNhssiDoXaycUvOUS58266yPDlitPIAzO03XClm4EDPXGIwcwiFr7FcDo3tQIMZVy87i48Zb80s3zAYRiBIS0vO3RKGx3OGN5zid2B7MfnfLzvpvgZoirHhAqXffnym5abpZNzGuo5GowTRA2Ptk4Ve2JFoHACWpD6HiGnRZ9QVOmPICoQrSUQw45Jlk9onKJz5Erhnx0943Uno6tMJ5jbrWBNiIO7i04xzRBgujeiAJvuQkVDX2QLKRxZ7s6rhdfOaq6R6uL108gEzzlXOLqTTJXgM63rcUWNbE7wsIXcCFSF59LLJ7G5Qea33suxdDX6DcK4a0VMZoxmWPtCi1dAT9ggJqc2Sh7mkAqizaB16RXZvSydchpdVj6s4qn4ivr0HKHdAstX0XZ0FFU6lOiNmU3vasMg2uaVG8tyuG8N8VsuXIOQs7xtFxDhilYb8MQ9vES9pWfWPSXFlJAq4XKPY8a0JOIx57EQuWHo3uWgRTIRThvZP9YYzSnjGIHwjS8JeppICHofADXZhJ0uDQaQs7MiXEALpGmT3W6w0G3tBdZcuTDkWx1HsT5jd9jQeJpgD2VxdKh8U4Q3vANTAuwBXLJ2P0stS8Q72JWgNPwKYTY9cPoaGZlUFGgVsq8CdEFH9yW0c27G5s5sfHsyep6t4VxIHHMOX2GmMRyGxDI33am1J7ZmJ1NyXiwkHxtPH5QBpU2PMu2Guf3xIxlk3snMkMAsGO0vYfqO9tdIgdxMYO3HZTYv99OXaHcNQ5u0pRZZyVrNOIPurkEOdJy0nowPemIgUuHWh8vQCuDZav1m35AOl6ftSFuChSm5KstEWnC7q8mJ0juJEBkCRmQphP3V1pqiDjz6YA90qEe7MA3nzT0nHG8A1hWlqcPVPNz4qWNF6Fq1ub4075aXO0H7Krb6rhWGb3ZRPjpb4BKN8jGFQrBUMZprtjAJ67BnfmYgE0mmGLV2QP10gYS1T06kBRyrtp7he6wsPiBPJ7wxPLHNUN2SGQHBTSKagndM99fuaga5Sw9OT8Fzdo7xUJXfhJ97gUnNDrknal0B00NMNvajZeQQTJyBsVSwBZtZ45ZCcq1idc7GWC0MITSk58cIVkSPXbrERUaygyY13dPeEVzjVi9aVJwUF6eJu1s8u3FCJqp2GoWIItwvZO69asX75fekFkmFpNavxM0X0dZC01TTPpV6E6PJoIfW8C06CKNHV7Gk2mkTWGSwUG4xD2L3G3XarodHDcmumFJX9Xviv0rvm38SCtin6OpjH8MHYDrj1OxTJbC2VclJxv73z2BDBquosKOik0fmgbPZN0FUTmjBEwHTvqd5QHTwb3nOpEz3X6YCF0lrcrQc0uhyr7gBGBs86nUBWFRp1LKjIRVTVXDipajqNDTQGNZtzvR9MUf1yJJV07inbrlPOENd7rHpKCrJtoZXOkDqInaIqoMCG3DVd353BGmZNJEKOa3DnL7fb9zwuHlvHAfCco7ZS4wAV87trWkp6skXux9v5WhkumbUyGq4ia6DM1PuqqnFfBTAWDzJsnggAJrzr8O7JbDtaXwcW9sqaOb0S6NvnUDZqiNdDQPMDOKvXRJJJQdf1FSrPCCSPEEWO1SeVwictj7rTbpWGRoukwhgJALys95pGGOQxCPzRGrtVFnGcsLN1CwI3wLbmDnNKUv3KpOLEOPRxQXeXuJRIiYCFum44c0wNr731DvHn3YEJMH4iwFONl1rolEL4w6KFUOCq7ekrE5iyUt1V32PNtuUshXRjOYjBval29JMH5GoqZlGhCczzHMA61cmuzqdFwiPCB9yzqvJTg8TqMNvwKJztFIQK4mc5Ev5rRVSozD796AVRKT8rZF39IA1kmCLdXqz7CCC8x4QjjDpxjKCXP5HkWf9mp2FNBjE3pAeaEc6Vk2ENLlW8WVCe08aP8931Ltyl9nqyJvjMaRCOgDV3uONtAdHABjoZUG6KAP6h3Vh97O3GJjjovXYgNdrhxc7TriXoAmeehZMJx88EyhcPXO0f09Nvd128SZnxZ2r5jFDELkn26reKRysODSLBZLfjU3vxLzLXKWeFOFJKcZYRH9V7hC98DDS4ZsS7weUksBuK6m86aLNHHHB0Xbyxv1TiDbOWYIzKxV0eZKyk0CaDLDiR0CRuMOf4rwBeuHoMrumzafrFI5iL72ANQZmOvKdk1qQeXkRqEG11YU0kF7f1hSlmgiIgg5maWiBsA9sAg36IIXZMWwJF63zpMgAyjTT8l4pQhSBfhY2xbGAWmLGpyd1rlBm0O5LCoKpnQuTACm2azi0x6a1Qbry9flQBO4jHge2dXiD1si6Gh5q8fZu8ZQ7LLWii2u4rGB7E4XlhnClrCHg5vJmjYf2AItYPA0ogsiIdEEQGpzMJPqrp8Icn5kAAimWF1aCYaDjcdSgWI48PnoxlzIHX50EPFcPOSLecjkstD9z66H554sUXfWn3Mk9lnOUlse6nx0u1YClFK4UFXp98ru9eBBr7pkAsfZ34yPskayGyXPPyzWyBfVd28UuvdEG47SMdyqEpX0rFdk67fAYij0PWMK79mDmGAS37O821o18XUbu0GQjsqAGVMN9LDIAliD9QqtlwdEnplKkUyyZ7GAFJCFffgzppU9CjA2FbPX6ZjTOi4sPoYEyhyeQKVqAe9keYeDpU2qDwq83XEDQUKvP0w48GyavSmdBcrMXjUsu0PfdYpSaKwarrUB3i93HgoQB3ZJIR4lW6iPRTmm28OEKq2MIJGAoTXxCZYM5UacRldlqQOj6JkYz6y7ppWOjJ9yiCUEenuvfcItgmw9HIgGA59JxO8NDLEZLSONfuIgiV7wjsJnxuTOlU4vkjV7fTuOeU91xez7UKhaTqqEW3XBUSLjhKi3IkZg7ukrGZTWPhijFv2EZwEWDAyLlHvZB4X738zGJUlEX1k52EHwrKVKdLfePcaOjAGKsongHBFYxYC8vBBLuKm9RWexKCT14M25pCGloJXZ4OpBRfDQA2kobLUcEXEpzqRBPGN2JdNSBOFlUtUxWKnnPBM6r9S356l3k1o9zTIPeoIitWRjASs4A0iwYc8p5vv5Kt8KtsmW7Xv8dlU8HbZHsy3LI7O9BpUH8cJubqdEhooKABkx71pdcsZGhZb6epyTiPyvOhdJ7tNtFy3KQOameqTgGyd53Z42eZ0AjaOEvnzermi2E0xo3MMHFhB74TFtNAI3ppxxyqknc1mzUqZ49Wi8YPBg9ids6IgZvddBQYvwEozkmyGAkatQtt9TD4LjU3TyyUlhNG21q7CzEEl8NNsVrV6QyHsfw7E5w7XcoT7OQkBYoZwHIAjfekehnpc2llRtRY5m43fPVasmsVazOR36DRSLZJPHAqUDO0LInu9mgP57Mnz9CgylEmdE2aaYs426rnTFR3G3CfjLofHfjaLOkAegr4W3jx6MNMMOMZw2u46YTCnlfbBK6ZA1UYeAH1DIQJykcSQESinC8HpYIJt9A8g7UT0awzRP1F9nHa3wDnaAHndQYKMrjzlWo8ejQ0XHWgHhqnWHgW4h9sOnJckH00CYK1fHUKASJ3D8kOKax6uplexfz6BCvAoL9zm5TjeB1yxrpLp9NjjTWSKG2HOZhPkGpdEqU4mjnN2AkUVACPGos5YLBmTnSrdOEGZJDlAvJOUt800Mu3BYc1MiDIB6LMSSV5RsIUDFOzNletGQoq4G3yHZmx78uEse5vUTPFF3KT8LCrssqdIU9H97Npgf6N5j8arQ7ykLzN459jJaUzpGIo6uowPnUSatDf9GAvAmWNvsVTz6bYiAV71C7QF0C7UolYIQY6DHJEHejgX2YMEovWNLPL50eeC51h4DdPNv5G4ZdNtQTRVybYBZMpetGDiFmXN0JKa1sKHOSZxdrhKjxDIhrYVyCcRUMQ0sjGGHFuOcRszr6E5igEMtsebHQ3KYiGd5B27LikpUHhk61rgZlulHdMoS6YgQs6SV6UMVNku6sCw529xhUciDwRMhsbAjDlahYbrGa3NryxyV5LrXONGGKCchCqv7vDMdAtPrVr8M2vL5MySQAC3g90iugGQcLH3hCf9f1Kn5X0hM4KZTfwOPJhlfJsMRNhssiDoXaycUvOUS58266yPDlitPIAzO03XClm4EDPXGIwcwiFr7FcDo3tQIMZVy87i48Zb80s3zAYRiBIS0vO3RKGx3OGN5zid2B7MfnfLzvpvgZoirHhAqXffnym5abpZNzGuo5GowTRA2Ptk4Ve2JFoHACWpD6HiGnRZ9QVOmPICoQrSUQw45Jlk9onKJz5Erhnx0943Uno6tMJ5jbrWBNiIO7i04xzRBgujeiAJvuQkVDX2QLKRxZ7s6rhdfOaq6R6uL108gEzzlXOLqTTJXgM63rcUWNbE7wsIXcCFSF59LLJ7G5Qea33suxdDX6DcK4a0VMZoxmWPtCi1dAT9ggJqc2Sh7mkAqizaB16RXZvSydchpdVj6s4qn4ivr0HKHdAstX0XZ0FFU6lOiNmU3vasMg2uaVG8tyuG8N8VsuXIOQs7xtFxDhilYb8MQ9vES9pWfWPSXFlJAq4XKPY8a0JOIx57EQuWHo3uWgRTIRThvZP9YYzSnjGIHwjS8JeppICHofADXZhJ0uDQaQs7MiXEALpGmT3W6w0G3tBdZcuTDkWx1HsT5jd9jQeJpgD2VxdKh8U4Q3vANTAuwBXLJ2P0stS8Q72JWgNPwKYTY9cPoaGZlUFGgVsq8CdEFH9yW0c27G5s5sfHsyep6t4VxIHHMOX2GmMRyGxDI33am1J7ZmJ1NyXiwkHxtPH5QBpU2PMu2Guf3xIxlk3snMkMAsGO0vYfqO9tdIgdxMYO3HZTYv99OXaHcNQ5u0pRZZyVrNOIPurkEOdJy0nowPemIgUuHWh8vQCuDZav1m35AOl6ftSFuChSm5KstEWnC7q8mJ0juJEBkCRmQphP3V1pqiDjz6YA90qEe7MA3nzT0nHG8A1hWlqcPVPNz4qWNF6Fq1ub4075aXO0H7Krb6rhWGb3ZRPjpb4BKN8jGFQrBUMZprtjAJ67BnfmYgE0mmGLV2QP10gYS1T06kBRyrtp7he6wsPiBPJ7wxPLHNUN2SGQHBTSKagndM99fuaga5Sw9OT8Fzdo7xUJXfhJ97gUnNDrknal0B00NMNvajZeQQTJyBsVSwBZtZ45ZCcq1idc7GWC0MITSk58cIVkSPXbrERUaygyY13dPeEVzjVi9aVJwUF6eJu1s8u3FCJqp2GoWIItwvZO69asX75fekFkmFpNavxM0X0dZC01TTPpV6E6PJoIfW8C06CKNHV7Gk2mkTWGSwUG4xD2L3G3XarodHDcmumFJX9Xviv0rvm38SCtin6OpjH8MHYDrj1OxTJbC2VclJxv73z2BDBquosKOik0fmgbPZN0FUTmjBEwHTvqd5QHTwb3nOpEz3X6YCF0lrcrQc0uhyr7gBGBs86nUBWFRp1LKjIRVTVXDipajqNDTQGNZtzvR9MUf1yJJV07inbrlPOENd7rHpKCrJtoZXOkDqInaIqoMCG3DVd353BGmZNJEKOa3DnL7fb9zwuHlvHAfCco7ZS4wAV87trWkp6skXux9v5WhkumbUyGq4ia6DM1PuqqnFfBTAWDzJsnggAJrzr8O7JbDtaXwcW9sqaOb0S6NvnUDZqiNdDQPMDOKvXRJJJQdf1FSrPCCSPEEWO1SeVwictj7rTbpWGRoukwhgJALys95pGGOQxCPzRGrtVFnGcsLN1CwI3wLbmDnNKUv3KpOLEOPRxQXeXuJRIiYCFum44c0wNr731DvHn3YEJMH4iwFONl1rolEL4w6KFUOCq7ekrE5iyUt1V32PNtuUshXRjOYjBval29JMH5GoqZlGhCczzHMA61cmuzqdFwiPCB9yzqvJTg8TqMNvwKJztFIQK4mc5Ev5rRVSozD796AVRKT8rZF39IA1kmCLdXqz7CCC8x4QjjDpxjKCXP5HkWf9mp2FNBjE3pAeaE\\r\\n\nSomeheader: 08aP8931Ltyl9nqyJvjMaRCOgDV3uONtAdHABjoZUG6KAP6h3Vh97O3GJjjovXYgNdrhxc7TriXoAmeehZMJx88EyhcPXO0f09Nvd128SZnxZ2r5jFDELkn26reKRysODSLBZLfjU3vxLzLXKWeFOFJKcZYRH9V7hC98DDS4ZsS7weUksBuK6m86aLNHHHB0Xbyxv1TiDbOWYIzKxV0eZKyk0CaDLDiR0CRuMOf4rwBeuHoMrumzafrFI5iL72ANQZmOvKdk1qQeXkRqEG11YU0kF7f1hSlmgiIgg5maWiBsA9sAg36IIXZMWwJF63zpMgAyjTT8l4pQhSBfhY2xbGAWmLGpyd1rlBm0O5LCoKpnQuTACm2azi0x6a1Qbry9flQBO4jHge2dXiD1si6Gh5q8fZu8ZQ7LLWii2u4rGB7E4XlhnClrCHg5vJmjYf2AItYPA0ogsiIdEEQGpzMJPqrp8Icn5kAAimWF1aCYaDjcdSgWI48PnoxlzIHX50EPFcPOSLecjkstD9z66H554sUXfWn3Mk9lnOUlse6nx0u1YClFK4UFXp98ru9eBBr7pkAsfZ34yPskayGyXPPyzWyBfVd28UuvdEG47SMdyqEpX0rFdk67fAYij0PWMK79mDmGAS37O821o18XUbu0GQjsqAGVMN9LDIAliD9QqtlwdEnplKkUyyZ7GAFJCFffgzppU9CjA2FbPX6ZjTOi4sPoYEyhyeQKVqAe9keYeDpU2qDwq83XEDQUKvP0w48GyavSmdBcrMXjUsu0PfdYpSaKwarrUB3i93HgoQB3ZJIR4lW6iPRTmm28OEKq2MIJGAoTXxCZYM5UacRldlqQOj6JkYz6y7ppWOjJ9yiCUEenuvfcItgmw9HIgGA59JxO8NDLEZLSONfuIgiV7wjsJnxuTOlU4vkjV7fTuOeU91xez7UKhaTqqEW3XBUSLjhKi3IkZg7ukrGZTWPhijFv2EZwEWDAyLlHvZB4X738zGJUlEX1k52EHwrKVKdLfePcaOjAGKsongHBFYxYC8vBBLuKm9RWexKCT14M25pCGloJXZ4OpBRfDQA2kobLUcEXEpzqRBPGN2JdNSBOFlUtUxWKnnPBM6r9S356l3k1o9zTIPeoIitWRjASs4A0iwYc8p5vv5Kt8KtsmW7Xv8dlU8HbZHsy3LI7O9BpUH8cJubqdEhooKABkx71pdcsZGhZb6epyTiPyvOhdJ7tNtFy3KQOameqTgGyd53Z42eZ0AjaOEvnzermi2E0xo3MMHFhB74TFtNAI3ppxxyqknc1mzUqZ49Wi8YPBg9ids6IgZvddBQYvwEozkmyGAkatQtt9TD4LjU3TyyUlhNG21q7CzEEl8NNsVrV6QyHsfw7E5w7XcoT7OQkBYoZwHIAjfekehnpc2llRtRY5m43fPVasmsVazOR36DRSLZJPHAqUDO0LInu9mgP57Mnz9CgylEmdE2aaYs426rnTFR3G3CfjLofHfjaLOkAegr4W3jx6MNMMOMZw2u46YTCnlfbBK6ZA1UYeAH1DIQJykcSQESinC8HpYIJt9A8g7UT0awzRP1F9nHa3wDnaAHndQYKMrjzlWo8ejQ0XHWgHhqnWHgW4h9sOnJckH00CYK1fHUKASJ3D8kOKax6uplexfz6BCvAoL9zm5TjeB1yxrpLp9NjjTWSKG2HOZhPkGpdEqU4mjnN2AkUVACPGos5YLBmTnSrdOEGZJDlAvJOUt800Mu3BYc1MiDIB6LMSSV5RsIUDFOzNletGQoq4G3yHZmx78uEse5vUTPFF3KT8LCrssqdIU9H97Npgf6N5j8arQ7ykLzN459jJaUzpGIo6uowPnUSatDf9GAvAmWNvsVTz6bYiAV71C7QF0C7UolYIQY6DHJEHejgX2YMEovWNLPL50eeC51h4DdPNv5G4ZdNtQTRVybYBZMpetGDiFmXN0JKa1sKHOSZxdrhKjxDIhrYVyCcRUMQ0sjGGHFuOcRszr6E5igEMtsebHQ3KYiGd5B27LikpUHhk61rgZlulHdMoS6YgQs6SV6UMVNku6sCw529xhUciDwRMhsbAjDlahYbrGa3NryxyV5LrXONGGKCchCqv7vDMdAtPrVr8M2vL5MySQAC3g90iugGQcLH3hCf9f1Kn5X0hM4KZTfwOPJhlfJsMRNhssiDoXaycUvOUS58266yPDlitPIAzO03XClm4EDPXGIwcwiFr7FcDo3tQIMZVy87i48Zb80s3zAYRiBIS0vO3RKGx3OGN5zid2B7MfnfLzvpvgZoirHhAqXffnym5abpZNzGuo5GowTRA2Ptk4Ve2JFoHACWpD6HiGnRZ9QVOmPICoQrSUQw45Jlk9onKJz5Erhnx0943Uno6tMJ5jbrWBNiIO7i04xzRBgujeiAJvuQkVDX2QLKRxZ7s6rhdfOaq6R6uL108gEzzlXOLqTTJXgM63rcUWNbE7wsIXcCFSF59LLJ7G5Qea33suxdDX6DcK4a0VMZoxmWPtCi1dAT9ggJqc2Sh7mkAqizaB16RXZvSydchpdVj6s4qn4ivr0HKHdAstX0XZ0FFU6lOiNmU3vasMg2uaVG8tyuG8N8VsuXIOQs7xtFxDhilYb8MQ9vES9pWfWPSXFlJAq4XKPY8a0JOIx57EQuWHo3uWgRTIRThvZP9YYzSnjGIHwjS8JeppICHofADXZhJ0uDQaQs7MiXEALpGmT3W6w0G3tBdZcuTDkWx1HsT5jd9jQeJpgD2VxdKh8U4Q3vANTAuwBXLJ2P0stS8Q72JWgNPwKYTY9cPoaGZlUFGgVsq8CdEFH9yW0c27G5s5sfHsyep6t4VxIHHMOX2GmMRyGxDI33am1J7ZmJ1NyXiwkHxtPH5QBpU2PMu2Guf3xIxlk3snMkMAsGO0vYfqO9tdIgdxMYO3HZTYv99OXaHcNQ5u0pRZZyVrNOIPurkEOdJy0nowPemIgUuHWh8vQCuDZav1m35AOl6ftSFuChSm5KstEWnC7q8mJ0juJEBkCRmQphP3V1pqiDjz6YA90qEe7MA3nzT0nHG8A1hWlqcPVPNz4qWNF6Fq1ub4075aXO0H7Krb6rhWGb3ZRPjpb4BKN8jGFQrBUMZprtjAJ67BnfmYgE0mmGLV2QP10gYS1T06kBRyrtp7he6wsPiBPJ7wxPLHNUN2SGQHBTSKagndM99fuaga5Sw9OT8Fzdo7xUJXfhJ97gUnNDrknal0B00NMNvajZeQQTJyBsVSwBZtZ45ZCcq1idc7GWC0MITSk58cIVkSPXbrERUaygyY13dPeEVzjVi9aVJwUF6eJu1s8u3FCJqp2GoWIItwvZO69asX75fekFkmFpNavxM0X0dZC01TTPpV6E6PJoIfW8C06CKNHV7Gk2mkTWGSwUG4xD2L3G3XarodHDcmumFJX9Xviv0rvm38SCtin6OpjH8MHYDrj1OxTJbC2VclJxv73z2BDBquosKOik0fmgbPZN0FUTmjBEwHTvqd5QHTwb3nOpEz3X6YCF0lrcrQc0uhyr7gBGBs86nUBWFRp1LKjIRVTVXDipajqNDTQGNZtzvR9MUf1yJJV07inbrlPOENd7rHpKCrJtoZXOkDqInaIqoMCG3DVd353BGmZNJEKOa3DnL7fb9zwuHlvHAfCco7ZS4wAV87trWkp6skXux9v5WhkumbUyGq4ia6DM1PuqqnFfBTAWDzJsnggAJrzr8O7JbDtaXwcW9sqaOb0S6NvnUDZqiNdDQPMDOKvXRJJJQdf1FSrPCCSPEEWO1SeVwictj7rTbpWGRoukwhgJALys95pGGOQxCPzRGrtVFnGcsLN1CwI3wLbmDnNKUv3KpOLEOPRxQXeXuJRIiYCFum44c0wNr731DvHn3YEJMH4iwFONl1rolEL4w6KFUOCq7ekrE5iyUt1V32PNtuUshXRjOYjBval29JMH5GoqZlGhCczzHMA61cmuzqdFwiPCB9yzqvJTg8TqMNvwKJztFIQK4mc5Ev5rRVSozD796AVRKT8rZF39IA1kmCLdXqz7CCC8x4QjjDpxjKCXP5HkWf9mp2FNBjE3pAeaEc6Vk2ENLlW8WVCe\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/008.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import LimitRequestHeaders\nrequest = LimitRequestHeaders\n"
  },
  {
    "path": "tests/requests/invalid/009.http",
    "content": "PUT /stuff/here?foo=bar HTTP/1.0\\r\\n\nServer: http://127.0.0.1:5984\\r\\n\nContent-Type: application/json\\r\\n\nContent-Length: 14\\r\\n\nheader0: 0\\r\\n\nheader1: 1\\r\\n\nheader2: 2\\r\\n\nheader3: 3\\r\\n\nheader4: 4\\r\\n\nheader5: 5\\r\\n\nheader6: 6\\r\\n\nheader7: 7\\r\\n\nheader8: 8\\r\\n\nheader9: 9\\r\\n\nheader10: 10\\r\\n\nheader11: 11\\r\\n\nheader12: 12\\r\\n\nheader13: 13\\r\\n\nheader14: 14\\r\\n\nheader15: 15\\r\\n\nheader16: 16\\r\\n\nheader17: 17\\r\\n\nheader18: 18\\r\\n\nheader19: 19\\r\\n\nheader20: 20\\r\\n\nheader21: 21\\r\\n\nheader22: 22\\r\\n\nheader23: 23\\r\\n\nheader24: 24\\r\\n\nheader25: 25\\r\\n\nheader26: 26\\r\\n\nheader27: 27\\r\\n\nheader28: 28\\r\\n\nheader29: 29\\r\\n\nheader30: 30\\r\\n\nheader31: 31\\r\\n\nheader32: 32\\r\\n\nheader33: 33\\r\\n\nheader34: 34\\r\\n\nheader35: 35\\r\\n\nheader36: 36\\r\\n\nheader37: 37\\r\\n\nheader38: 38\\r\\n\nheader39: 39\\r\\n\nheader40: 40\\r\\n\nheader41: 41\\r\\n\nheader42: 42\\r\\n\nheader43: 43\\r\\n\nheader44: 44\\r\\n\nheader45: 45\\r\\n\nheader46: 46\\r\\n\nheader47: 47\\r\\n\nheader48: 48\\r\\n\nheader49: 49\\r\\n\nheader50: 50\\r\\n\nheader51: 51\\r\\n\nheader52: 52\\r\\n\nheader53: 53\\r\\n\nheader54: 54\\r\\n\nheader55: 55\\r\\n\nheader56: 56\\r\\n\nheader57: 57\\r\\n\nheader58: 58\\r\\n\nheader59: 59\\r\\n\nheader60: 60\\r\\n\nheader61: 61\\r\\n\nheader62: 62\\r\\n\nheader63: 63\\r\\n\nheader64: 64\\r\\n\nheader65: 65\\r\\n\nheader66: 66\\r\\n\nheader67: 67\\r\\n\nheader68: 68\\r\\n\nheader69: 69\\r\\n\nheader70: 70\\r\\n\nheader71: 71\\r\\n\nheader72: 72\\r\\n\nheader73: 73\\r\\n\nheader74: 74\\r\\n\nheader75: 75\\r\\n\nheader76: 76\\r\\n\nheader77: 77\\r\\n\nheader78: 78\\r\\n\nheader79: 79\\r\\n\nheader80: 80\\r\\n\nheader81: 81\\r\\n\nheader82: 82\\r\\n\nheader83: 83\\r\\n\nheader84: 84\\r\\n\nheader85: 85\\r\\n\nheader86: 86\\r\\n\nheader87: 87\\r\\n\nheader88: 88\\r\\n\nheader89: 89\\r\\n\nheader90: 90\\r\\n\nheader91: 91\\r\\n\nheader92: 92\\r\\n\nheader93: 93\\r\\n\nheader94: 94\\r\\n\nheader95: 95\\r\\n\nheader96: 96\\r\\n\nheader97: 97\\r\\n\nheader98: 98\\r\\n\nheader99: 99\\r\\n\n\\r\\n\n{\"nom\": \"nom\"}\n"
  },
  {
    "path": "tests/requests/invalid/009.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import LimitRequestHeaders\nrequest = LimitRequestHeaders\n"
  },
  {
    "path": "tests/requests/invalid/010.http",
    "content": "GET /test HTTP/1.1\\r\\n\nAccept: */*\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/010.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\nfrom gunicorn.http.errors import LimitRequestHeaders\n\nrequest = LimitRequestHeaders\ncfg = Config()\ncfg.set('limit_request_field_size', 10)\n"
  },
  {
    "path": "tests/requests/invalid/011.http",
    "content": "GET /test HTTP/1.1\\r\\n\nUser-Agent: curl/7.18.0 (i486-pc-linux-gnu) libcurl/7.18.0 OpenSSL/0.9.8g zlib/1.2.3.3 libidn/1.1\\r\\n\nHost: 0.0.0.0=5000\\r\\n\nAccept: */*\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/011.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\nfrom gunicorn.http.errors import LimitRequestHeaders\n\nrequest = LimitRequestHeaders\ncfg = Config()\ncfg.set('limit_request_fields', 2)\n"
  },
  {
    "path": "tests/requests/invalid/012.http",
    "content": "GET /test HTTP/1.1\\r\\n\nUser-Agent: curl/7.18.0 (i486-pc-linux-gnu) libcurl/7.18.0 OpenSSL/0.9.8g zlib/1.2.3.3 libidn/1.1\\r\\n\nHost: 0.0.0.0=5000\\r\\n\nAccept: */*\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/012.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\nfrom gunicorn.http.errors import LimitRequestHeaders\n\nrequest = LimitRequestHeaders\ncfg = Config()\ncfg.set('limit_request_field_size', 98)\n"
  },
  {
    "path": "tests/requests/invalid/013.http",
    "content": "GET /test HTTP/1.1\\r\\n\nAccept:\\r\\n\n */*\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/013.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\nfrom gunicorn.http.errors import LimitRequestHeaders\n\nrequest = LimitRequestHeaders\ncfg = Config()\ncfg.set('limit_request_field_size', 14)\n\n# once this option is removed, this test should not be dropped;\n#  rather, add something involving unnessessary padding\ncfg.set('permit_obsolete_folding', True)\n"
  },
  {
    "path": "tests/requests/invalid/014.http",
    "content": "PUT /stuff/here?foo=bar HTTP/1.0\\r\\n\nCONTENT-LENGTH: -1\\r\\n\n\\r\\n\n{\"test\": \"-1}"
  },
  {
    "path": "tests/requests/invalid/014.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import InvalidHeader\n\nrequest = InvalidHeader\n"
  },
  {
    "path": "tests/requests/invalid/015.http",
    "content": "POST /stuff/here?foo=bar HTTP/1.0\\r\\n\nCONTENT-LENGTH: bla-bla-bla\\r\\n\n\\r\\n\n{\"test\": \"-1}"
  },
  {
    "path": "tests/requests/invalid/015.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import InvalidHeader\n\nrequest = InvalidHeader\n"
  },
  {
    "path": "tests/requests/invalid/016.http",
    "content": "PUT s://]ufd/: HTTP/1.1\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/016.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import InvalidRequestLine\n\nrequest = InvalidRequestLine\n"
  },
  {
    "path": "tests/requests/invalid/017.http",
    "content": "GET /test HTTP/1.1\\r\\n\nLong-header: 0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/017.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\nfrom gunicorn.http.errors import LimitRequestHeaders\n\ncfg = Config()\nrequest = LimitRequestHeaders\n"
  },
  {
    "path": "tests/requests/invalid/018.http",
    "content": "GET /test HTTP/111\\r\\n\nHost: localhost\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/018.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import InvalidHTTPVersion\nrequest = InvalidHTTPVersion\n"
  },
  {
    "path": "tests/requests/invalid/019.http",
    "content": "GET /test HTTP/1.1\\r\\n\nX-Forwarded-Proto: https\\r\\n\nX-Forwarded-Ssl: off\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/019.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\nfrom gunicorn.http.errors import InvalidSchemeHeaders\n\nrequest = InvalidSchemeHeaders\ncfg = Config()\ncfg.set('forwarded_allow_ips', '*')\n"
  },
  {
    "path": "tests/requests/invalid/020.http",
    "content": "GET /stuff/here?foo=bar HTTP/1.1\\r\\n\nContent-Length : 3\\r\\n\n\\r\\n\nxyz\n"
  },
  {
    "path": "tests/requests/invalid/020.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\nfrom gunicorn.http.errors import InvalidHeaderName\n\ncfg = Config()\nrequest = InvalidHeaderName\n"
  },
  {
    "path": "tests/requests/invalid/021.http",
    "content": "GET /stuff/here?foo=bar HTTP/1.1\\r\\n\nContent-Length: 3\\r\\n\nContent-Length: 2\\r\\n\n\\r\\n\nxyz\n"
  },
  {
    "path": "tests/requests/invalid/021.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\nfrom gunicorn.http.errors import InvalidHeader\n\ncfg = Config()\nrequest = InvalidHeader\n"
  },
  {
    "path": "tests/requests/invalid/022.http",
    "content": "GET /first HTTP/1.0\\r\\n\nContent-Length: -0\\r\\n\n\\r\\n"
  },
  {
    "path": "tests/requests/invalid/022.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\nfrom gunicorn.http.errors import InvalidHeader\n\ncfg = Config()\nrequest = InvalidHeader\n"
  },
  {
    "path": "tests/requests/invalid/023.http",
    "content": "GET /first HTTP/1.0\\r\\n\nContent-Length: 0_1\\r\\n\n\\r\\n"
  },
  {
    "path": "tests/requests/invalid/023.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\nfrom gunicorn.http.errors import InvalidHeader\n\ncfg = Config()\nrequest = InvalidHeader\n"
  },
  {
    "path": "tests/requests/invalid/024.http",
    "content": "GET /first HTTP/1.0\\r\\n\nContent-Length: +1\\r\\n\n\\r\\n"
  },
  {
    "path": "tests/requests/invalid/024.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\nfrom gunicorn.http.errors import InvalidHeader\n\ncfg = Config()\nrequest = InvalidHeader\n"
  },
  {
    "path": "tests/requests/invalid/040.http",
    "content": "GET /keep/same/as?invalid/040 HTTP/1.0\\r\\n\nTransfer_Encoding: tricked\\r\\n\nContent-Length: 7\\r\\n\nContent_Length: -1E23\\r\\n\n\\r\\n\ntricked\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/040.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import InvalidHeaderName\nfrom gunicorn.config import Config\n\ncfg = Config()\ncfg.set(\"header_map\", \"refuse\")\n\nrequest = InvalidHeaderName\n"
  },
  {
    "path": "tests/requests/invalid/chunked_01.http",
    "content": "POST /chunked_w_underscore_chunk_size HTTP/1.1\\r\\n\nTransfer-Encoding: chunked\\r\\n\n\\r\\n\n5\\r\\n\nhello\\r\\n\n6_0\\r\\n\n world\\r\\n\n0\\r\\n\n\\r\\n\nPOST /after HTTP/1.1\\r\\n\nTransfer-Encoding: identity\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/chunked_01.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import InvalidChunkSize\nrequest = InvalidChunkSize\n"
  },
  {
    "path": "tests/requests/invalid/chunked_02.http",
    "content": "POST /chunked_with_prefixed_value HTTP/1.1\\r\\n\nContent-Length: 12\\r\\n\nTransfer-Encoding: \\tchunked\\r\\n\n\\r\\n\n5\\r\\n\nhello\\r\\n\n6\\r\\n\n world\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/chunked_02.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import InvalidHeader\nrequest = InvalidHeader\n"
  },
  {
    "path": "tests/requests/invalid/chunked_03.http",
    "content": "POST /double_chunked HTTP/1.1\\r\\n\nTransfer-Encoding: identity, chunked, identity, chunked\\r\\n\n\\r\\n\n5\\r\\n\nhello\\r\\n\n6\\r\\n\n world\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/chunked_03.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import InvalidHeader \nrequest = InvalidHeader \n"
  },
  {
    "path": "tests/requests/invalid/chunked_04.http",
    "content": "POST /chunked_twice HTTP/1.1\\r\\n\nTransfer-Encoding: identity\\r\\n\nTransfer-Encoding: chunked\\r\\n\nTransfer-Encoding: identity\\r\\n\nTransfer-Encoding: chunked\\r\\n\n\\r\\n\n5\\r\\n\nhello\\r\\n\n6\\r\\n\n world\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/chunked_04.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import InvalidHeader\nrequest = InvalidHeader\n"
  },
  {
    "path": "tests/requests/invalid/chunked_05.http",
    "content": "POST /chunked_HTTP_1.0 HTTP/1.0\\r\\n\nTransfer-Encoding: chunked\\r\\n\n\\r\\n\n5\\r\\n\nhello\\r\\n\n6\\r\\n\n world\\r\\n\n0\\r\\n\nVary: *\\r\\n\nContent-Type: text/plain\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/chunked_05.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import InvalidHeader\nrequest = InvalidHeader\n"
  },
  {
    "path": "tests/requests/invalid/chunked_06.http",
    "content": "POST /chunked_not_last HTTP/1.1\\r\\n\nTransfer-Encoding: chunked\\r\\n\nTransfer-Encoding: gzip\\r\\n\n\\r\\n\n5\\r\\n\nhello\\r\\n\n6\\r\\n\n world\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/chunked_06.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import InvalidHeader \nrequest = InvalidHeader \n"
  },
  {
    "path": "tests/requests/invalid/chunked_07.http",
    "content": "POST /chunked_ambiguous_header_mapping HTTP/1.1\\r\\n\nTransfer_Encoding: gzip\\r\\n\nTransfer-Encoding: chunked\\r\\n\n\\r\\n\n5\\r\\n\nhello\\r\\n\n6\\r\\n\n world\\r\\n\n0\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/chunked_07.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import InvalidHeaderName\nfrom gunicorn.config import Config\n\ncfg = Config()\ncfg.set(\"header_map\", \"refuse\")\n\nrequest = InvalidHeaderName\n"
  },
  {
    "path": "tests/requests/invalid/chunked_08.http",
    "content": "POST /chunked_not_last HTTP/1.1\\r\\n\nTransfer-Encoding: chunked\\r\\n\nTransfer-Encoding: identity\\r\\n\n\\r\\n\n5\\r\\n\nhello\\r\\n\n6\\r\\n\n world\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/chunked_08.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import InvalidHeader\nrequest = InvalidHeader\n"
  },
  {
    "path": "tests/requests/invalid/chunked_09.http",
    "content": "POST /chunked_ows_without_ext HTTP/1.1\\r\\n\nTransfer-Encoding: chunked\\r\\n\n\\r\\n\n5\\r\\n\nhello\\r\\n\n0 \\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/chunked_09.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import InvalidChunkSize\nrequest = InvalidChunkSize\n"
  },
  {
    "path": "tests/requests/invalid/chunked_10.http",
    "content": "POST /chunked_ows_before HTTP/1.1\\r\\n\nTransfer-Encoding: chunked\\r\\n\n\\r\\n\n5\\r\\n\nhello\\r\\n\n 0\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/chunked_10.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import InvalidChunkSize\nrequest = InvalidChunkSize\n"
  },
  {
    "path": "tests/requests/invalid/chunked_11.http",
    "content": "POST /chunked_ows_before HTTP/1.1\\r\\n\nTransfer-Encoding: chunked\\r\\n\n\\r\\n\n5\\n;\\r\\n\nhello\\r\\n\n0\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/chunked_11.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import InvalidChunkSize\nrequest = InvalidChunkSize\n"
  },
  {
    "path": "tests/requests/invalid/chunked_12.http",
    "content": "POST /chunked_no_chunk_size_but_ext HTTP/1.1\\r\\n\nTransfer-Encoding: chunked\\r\\n\n\\r\\n\n ;foo=bar\\r\\n\nhello\\r\\n\n0\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/chunked_12.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import InvalidChunkSize\nrequest = InvalidChunkSize\n"
  },
  {
    "path": "tests/requests/invalid/chunked_13.http",
    "content": "POST /chunked_no_chunk_size HTTP/1.1\\r\\n\nTransfer-Encoding: chunked\\r\\n\n\\r\\n\n\\r\\n\nhello\\r\\n\n0\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/chunked_13.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import InvalidChunkSize\nrequest = InvalidChunkSize\n"
  },
  {
    "path": "tests/requests/invalid/invalid_field_value_01.http",
    "content": "GET / HTTP/1.1\\r\\n\nHost: x\\r\\n\nNewline: a\\n\nContent-Length: 26\\r\\n\nGET / HTTP/1.1\\n\nHost: x\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/invalid_field_value_01.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\nfrom gunicorn.http.errors import InvalidHeader\n\ncfg = Config()\nrequest = InvalidHeader\n"
  },
  {
    "path": "tests/requests/invalid/nonascii_01.http",
    "content": "GETß /germans.. HTTP/1.1\\r\\n\nContent-Length: 3\\r\\n\n\\r\\n\nÄÄÄ\n"
  },
  {
    "path": "tests/requests/invalid/nonascii_01.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\nfrom gunicorn.http.errors import InvalidRequestMethod\n\ncfg = Config()\nrequest = InvalidRequestMethod\n"
  },
  {
    "path": "tests/requests/invalid/nonascii_02.http",
    "content": "GETÿ /french.. HTTP/1.1\\r\\n\nContent-Length: 3\\r\\n\n\\r\\n\nÄÄÄ\n"
  },
  {
    "path": "tests/requests/invalid/nonascii_02.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\nfrom gunicorn.http.errors import InvalidRequestMethod\n\ncfg = Config()\nrequest = InvalidRequestMethod\n"
  },
  {
    "path": "tests/requests/invalid/nonascii_03.http",
    "content": "GET /germans.. HTTP/1.1\\r\\n\nContent-Lengthß: 3\\r\\n\nContent-Length: 3\\r\\n\n\\r\\n\nÄÄÄ\n"
  },
  {
    "path": "tests/requests/invalid/nonascii_03.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\nfrom gunicorn.http.errors import InvalidHeaderName\n\ncfg = Config()\nrequest = InvalidHeaderName\n"
  },
  {
    "path": "tests/requests/invalid/nonascii_04.http",
    "content": "GET /french.. HTTP/1.1\\r\\n\nContent-Lengthÿ: 3\\r\\n\nContent-Length: 3\\r\\n\n\\r\\n\nÄÄÄ\n"
  },
  {
    "path": "tests/requests/invalid/nonascii_04.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\nfrom gunicorn.http.errors import InvalidHeaderName\n\ncfg = Config()\nrequest = InvalidHeaderName\n"
  },
  {
    "path": "tests/requests/invalid/obs_fold_01.http",
    "content": "GET / HTTP/1.1\\r\\n\nLong: one\\r\\n\n two\\r\\n\nHost: localhost\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/obs_fold_01.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import ObsoleteFolding\n\nrequest = ObsoleteFolding\n"
  },
  {
    "path": "tests/requests/invalid/pp_01.http",
    "content": "PROXY TCP4 192.168.0.1 192.16...\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/pp_01.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\nfrom gunicorn.http.errors import InvalidProxyLine\n\ncfg = Config()\ncfg.set(\"proxy_protocol\", True)\n\nrequest = InvalidProxyLine\n"
  },
  {
    "path": "tests/requests/invalid/pp_02.http",
    "content": "PROXY TCP4 192.168.0.1 192.168.0.11 65iii 100000\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/pp_02.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\nfrom gunicorn.http.errors import InvalidProxyLine\n\ncfg = Config()\ncfg.set('proxy_protocol', True)\n\nrequest = InvalidProxyLine\n"
  },
  {
    "path": "tests/requests/invalid/prefix_01.http",
    "content": "GET\\0PROXY /foo HTTP/1.1\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/prefix_01.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import InvalidRequestMethod\nrequest = InvalidRequestMethod"
  },
  {
    "path": "tests/requests/invalid/prefix_02.http",
    "content": "GET\\0 /foo HTTP/1.1\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/prefix_02.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import InvalidRequestMethod\nrequest = InvalidRequestMethod"
  },
  {
    "path": "tests/requests/invalid/prefix_03.http",
    "content": "GET /stuff/here?foo=bar HTTP/1.1\\r\\n\nContent-Length: 0 1\\r\\n\n\\r\\n\nx\n"
  },
  {
    "path": "tests/requests/invalid/prefix_03.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\nfrom gunicorn.http.errors import InvalidHeader\n\ncfg = Config()\nrequest = InvalidHeader\n"
  },
  {
    "path": "tests/requests/invalid/prefix_04.http",
    "content": "GET /stuff/here?foo=bar HTTP/1.1\\r\\n\nContent-Length: 3 1\\r\\n\n\\r\\n\nxyz\nabc123\n"
  },
  {
    "path": "tests/requests/invalid/prefix_04.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\nfrom gunicorn.http.errors import InvalidHeader\n\ncfg = Config()\nrequest = InvalidHeader\n"
  },
  {
    "path": "tests/requests/invalid/prefix_05.http",
    "content": "GET: /stuff/here?foo=bar HTTP/1.1\\r\\n\nContent-Length: 3\\r\\n\n\\r\\n\nxyz\n"
  },
  {
    "path": "tests/requests/invalid/prefix_05.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\nfrom gunicorn.http.errors import InvalidRequestMethod\n\ncfg = Config()\nrequest = InvalidRequestMethod\n"
  },
  {
    "path": "tests/requests/invalid/prefix_06.http",
    "content": "GET /the/future HTTP/1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111.1\\r\\n\nContent-Length: 7\\r\\n\n\\r\\n\nOld Man\n"
  },
  {
    "path": "tests/requests/invalid/prefix_06.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\nfrom gunicorn.http.errors import InvalidHTTPVersion\n\ncfg = Config()\nrequest = InvalidHTTPVersion\n"
  },
  {
    "path": "tests/requests/invalid/version_01.http",
    "content": "GET /foo HTTP/0.99\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/version_01.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import InvalidHTTPVersion\nrequest = InvalidHTTPVersion\n"
  },
  {
    "path": "tests/requests/invalid/version_02.http",
    "content": "GET /foo HTTP/2.0\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/invalid/version_02.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import InvalidHTTPVersion\nrequest = InvalidHTTPVersion\n"
  },
  {
    "path": "tests/requests/valid/001.http",
    "content": "PUT /stuff/here?foo=bar HTTP/1.0\\r\\n\nServer: http://127.0.0.1:5984\\r\\n\nContent-Type: application/json\\r\\n\nContent-Length: 14\\r\\n\n\\r\\n\n{\"nom\": \"nom\"}\n"
  },
  {
    "path": "tests/requests/valid/001.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nrequest = {\n    \"method\": \"PUT\",\n    \"uri\": uri(\"/stuff/here?foo=bar\"),\n    \"version\": (1, 0),\n    \"headers\": [\n        (\"SERVER\", \"http://127.0.0.1:5984\"),\n        (\"CONTENT-TYPE\", \"application/json\"),\n        (\"CONTENT-LENGTH\", \"14\")\n    ],\n    \"body\": b'{\"nom\": \"nom\"}'\n}\n"
  },
  {
    "path": "tests/requests/valid/002.http",
    "content": "GET /test HTTP/1.1\\r\\n\nUser-Agent: curl/7.18.0 (i486-pc-linux-gnu) libcurl/7.18.0 OpenSSL/0.9.8g zlib/1.2.3.3 libidn/1.1\\r\\n\nHost: 0.0.0.0=5000\\r\\n\nAccept: */*\\r\\n\n\\r\\n"
  },
  {
    "path": "tests/requests/valid/002.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nrequest = {\n    \"method\": \"GET\",\n    \"uri\": uri(\"/test\"),\n    \"version\": (1, 1),\n    \"headers\": [\n        (\"USER-AGENT\", \"curl/7.18.0 (i486-pc-linux-gnu) libcurl/7.18.0 OpenSSL/0.9.8g zlib/1.2.3.3 libidn/1.1\"),\n        (\"HOST\", \"0.0.0.0=5000\"),\n        (\"ACCEPT\", \"*/*\")\n    ],\n    \"body\": b\"\"\n}\n"
  },
  {
    "path": "tests/requests/valid/003.http",
    "content": "GET /favicon.ico HTTP/1.1\\r\\n\nHost: 0.0.0.0=5000\\r\\n\nUser-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9) Gecko/2008061015 Firefox/3.0\\r\\n\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\\r\\n\nAccept-Language: en-us,en;q=0.5\\r\\n\nAccept-Encoding: gzip,deflate\\r\\n\nAccept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\\r\\n\nKeep-Alive: 300\\r\\n\nConnection: keep-alive\\r\\n\n\\r\\n"
  },
  {
    "path": "tests/requests/valid/003.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nrequest = {\n    \"method\": \"GET\",\n    \"uri\": uri(\"/favicon.ico\"),\n    \"version\": (1, 1),\n    \"headers\": [\n        (\"HOST\", \"0.0.0.0=5000\"),\n        (\"USER-AGENT\", \"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9) Gecko/2008061015 Firefox/3.0\"),\n        (\"ACCEPT\", \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\"),\n        (\"ACCEPT-LANGUAGE\", \"en-us,en;q=0.5\"),\n        (\"ACCEPT-ENCODING\", \"gzip,deflate\"),\n        (\"ACCEPT-CHARSET\", \"ISO-8859-1,utf-8;q=0.7,*;q=0.7\"),\n        (\"KEEP-ALIVE\", \"300\"),\n        (\"CONNECTION\", \"keep-alive\")\n    ],\n    \"body\": b\"\"\n}\n"
  },
  {
    "path": "tests/requests/valid/004.http",
    "content": "GET /silly HTTP/1.1\\r\\n\naaaaaaaaaaaaa:++++++++++\\r\\n\n\\r\\n"
  },
  {
    "path": "tests/requests/valid/004.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nrequest = {\n    \"method\": \"GET\",\n    \"uri\": uri(\"/silly\"),\n    \"version\": (1, 1),\n    \"headers\": [\n        (\"AAAAAAAAAAAAA\", \"++++++++++\")\n    ],\n    \"body\": b\"\"\n}\n"
  },
  {
    "path": "tests/requests/valid/005.http",
    "content": "GET /forums/1/topics/2375?page=1#posts-17408 HTTP/1.1\\r\\n\n\\r\\n"
  },
  {
    "path": "tests/requests/valid/005.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nrequest = {\n    \"method\": \"GET\",\n    \"uri\": uri(\"/forums/1/topics/2375?page=1#posts-17408\"),\n    \"version\": (1, 1),\n    \"headers\": [],\n    \"body\": b\"\"\n}\n"
  },
  {
    "path": "tests/requests/valid/006.http",
    "content": "GET /get_no_headers_no_body/world HTTP/1.1\\r\\n\n\\r\\n"
  },
  {
    "path": "tests/requests/valid/006.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nrequest = {\n    \"method\": \"GET\",\n    \"uri\": uri(\"/get_no_headers_no_body/world\"),\n    \"version\": (1, 1),\n    \"headers\": [],\n    \"body\": b\"\"\n}\n"
  },
  {
    "path": "tests/requests/valid/007.http",
    "content": "GET /get_one_header_no_body HTTP/1.1\\r\\n\nAccept: */*\\r\\n\n\\r\\n"
  },
  {
    "path": "tests/requests/valid/007.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nrequest = {\n    \"method\": \"GET\",\n    \"uri\": uri(\"/get_one_header_no_body\"),\n    \"version\": (1, 1),\n    \"headers\": [\n        (\"ACCEPT\", \"*/*\")\n    ],\n    \"body\": b\"\"\n}\n"
  },
  {
    "path": "tests/requests/valid/008.http",
    "content": "GET /unusual_content_length HTTP/1.0\\r\\n\nconTENT-Length: 5\\r\\n\n\\r\\n\nHELLO"
  },
  {
    "path": "tests/requests/valid/008.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nrequest = {\n    \"method\": \"GET\",\n    \"uri\": uri(\"/unusual_content_length\"),\n    \"version\": (1, 0),\n    \"headers\": [\n        (\"CONTENT-LENGTH\", \"5\")\n    ],\n    \"body\": b\"HELLO\"\n}\n"
  },
  {
    "path": "tests/requests/valid/009.http",
    "content": "POST /post_identity_body_world?q=search#hey HTTP/1.1\\r\\n\nAccept: */*\\r\\n\nTransfer-Encoding: identity\\r\\n\nContent-Length: 5\\r\\n\n\\r\\n\nWorld"
  },
  {
    "path": "tests/requests/valid/009.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nrequest = {\n    \"method\": \"POST\",\n    \"uri\": uri(\"/post_identity_body_world?q=search#hey\"),\n    \"version\": (1, 1),\n    \"headers\": [\n        (\"ACCEPT\", \"*/*\"),\n        (\"TRANSFER-ENCODING\", \"identity\"),\n        (\"CONTENT-LENGTH\", \"5\")\n    ],\n    \"body\": b\"World\"\n}\n"
  },
  {
    "path": "tests/requests/valid/010.http",
    "content": "POST /post_chunked_all_your_base HTTP/1.1\\r\\n\nTransfer-Encoding: chunked\\r\\n\n\\r\\n\n1e\\r\\n\nall your base are belong to us\\r\\n\n0\\r\\n\n\\r\\n"
  },
  {
    "path": "tests/requests/valid/010.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nrequest = {\n    \"method\": \"POST\",\n    \"uri\": uri(\"/post_chunked_all_your_base\"),\n    \"version\": (1, 1),\n    \"headers\": [\n        (\"TRANSFER-ENCODING\", \"chunked\"),\n    ],\n    \"body\": b\"all your base are belong to us\"\n}\n"
  },
  {
    "path": "tests/requests/valid/011.http",
    "content": "POST /two_chunks_mult_zero_end HTTP/1.1\\r\\n\nTransfer-Encoding: chunked\\r\\n\n\\r\\n\n5\\r\\n\nhello\\r\\n\n6\\r\\n\n world\\r\\n\n000\\r\\n\n\\r\\n"
  },
  {
    "path": "tests/requests/valid/011.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nrequest = {\n    \"method\": \"POST\",\n    \"uri\": uri(\"/two_chunks_mult_zero_end\"),\n    \"version\": (1, 1),\n    \"headers\": [\n        (\"TRANSFER-ENCODING\", \"chunked\")\n    ],\n    \"body\": b\"hello world\"\n}\n"
  },
  {
    "path": "tests/requests/valid/012.http",
    "content": "POST /chunked_w_trailing_headers HTTP/1.1\\r\\n\nTransfer-Encoding: chunked\\r\\n\n\\r\\n\n5\\r\\n\nhello\\r\\n\n6\\r\\n\n world\\r\\n\n0\\r\\n\nVary: *\\r\\n\nContent-Type: text/plain\\r\\n\n\\r\\n"
  },
  {
    "path": "tests/requests/valid/012.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nrequest = {\n    \"method\": \"POST\",\n    \"uri\": uri(\"/chunked_w_trailing_headers\"),\n    \"version\": (1, 1),\n    \"headers\": [\n        (\"TRANSFER-ENCODING\", \"chunked\")\n    ],\n    \"body\": b\"hello world\",\n    \"trailers\": [\n        (\"VARY\", \"*\"),\n        (\"CONTENT-TYPE\", \"text/plain\")\n    ]\n}\n"
  },
  {
    "path": "tests/requests/valid/013.http",
    "content": "POST /chunked_w_extensions HTTP/1.1\\r\\n\nTransfer-Encoding: chunked\\r\\n\n\\r\\n\n5; some; parameters=stuff\\r\\n\nhello\\r\\n\n6; blahblah; blah\\r\\n\n world\\r\\n\n0\\r\\n\n\\r\\n"
  },
  {
    "path": "tests/requests/valid/013.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nrequest = {\n    \"method\": \"POST\",\n    \"uri\": uri(\"/chunked_w_extensions\"),\n    \"version\": (1, 1),\n    \"headers\": [\n        (\"TRANSFER-ENCODING\", \"chunked\")\n    ],\n    \"body\": b\"hello world\"\n}\n"
  },
  {
    "path": "tests/requests/valid/014.http",
    "content": "GET /with_\"quotes\"?foo=\"bar\" HTTP/1.1\\r\\n\n\\r\\n"
  },
  {
    "path": "tests/requests/valid/014.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nrequest = {\n    \"method\": \"GET\",\n    \"uri\": uri('/with_\"quotes\"?foo=\"bar\"'),\n    \"version\": (1, 1),\n    \"headers\": [],\n    \"body\": b\"\"\n}\n"
  },
  {
    "path": "tests/requests/valid/015.http",
    "content": "GET /test HTTP/1.0\\r\\n\nHost: 0.0.0.0:5000\\r\\n\nUser-Agent: ApacheBench/2.3\\r\\n\nAccept: */*\\r\\n\n\\r\\n"
  },
  {
    "path": "tests/requests/valid/015.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nrequest = {\n    \"method\": \"GET\",\n    \"uri\": uri(\"/test\"),\n    \"version\": (1, 0),\n    \"headers\": [\n        (\"HOST\", \"0.0.0.0:5000\"),\n        (\"USER-AGENT\", \"ApacheBench/2.3\"),\n        (\"ACCEPT\", \"*/*\")\n    ],\n    \"body\": b\"\"\n}\n"
  },
  {
    "path": "tests/requests/valid/017.http",
    "content": "GET /stuff/here?foo=bar HTTP/1.0\\r\\n\nIf-Match: bazinga!\\r\\n\nIf-Match: large-sound\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/valid/017.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nrequest = {\n    \"method\": \"GET\",\n    \"uri\": uri(\"/stuff/here?foo=bar\"),\n    \"version\": (1, 0),\n    \"headers\": [\n        (\"IF-MATCH\", \"bazinga!\"),\n        (\"IF-MATCH\", \"large-sound\")\n    ],\n    \"body\": b\"\"\n}\n"
  },
  {
    "path": "tests/requests/valid/018.http",
    "content": "GET /first HTTP/1.1\\r\\n\n\\r\\n\nGET /second HTTP/1.1\\r\\n\n\\r\\n"
  },
  {
    "path": "tests/requests/valid/018.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nreq1 = {\n    \"method\": \"GET\",\n    \"uri\": uri(\"/first\"),\n    \"version\": (1, 1),\n    \"headers\": [],\n    \"body\": b\"\"\n}\n\nreq2 = {\n    \"method\": \"GET\",\n    \"uri\": uri(\"/second\"),\n    \"version\": (1, 1),\n    \"headers\": [],\n    \"body\": b\"\"\n}\n\nrequest = [req1, req2]\n"
  },
  {
    "path": "tests/requests/valid/019.http",
    "content": "GET /first HTTP/1.0\\r\\n\n\\r\\n\nGET /second HTTP/1.1\\r\\n\n\\r\\n"
  },
  {
    "path": "tests/requests/valid/019.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nrequest = {\n    \"method\": \"GET\",\n    \"uri\": uri(\"/first\"),\n    \"version\": (1, 0),\n    \"headers\": [],\n    \"body\": b\"\"\n}\n"
  },
  {
    "path": "tests/requests/valid/020.http",
    "content": "GET /first HTTP/1.0\\r\\n\nContent-Length: 24\\r\\n\n\\r\\n\nGET /second HTTP/1.1\\r\\n\n\\r\\n"
  },
  {
    "path": "tests/requests/valid/020.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nrequest = {\n    \"method\": \"GET\",\n    \"uri\": uri(\"/first\"),\n    \"version\": (1, 0),\n    \"headers\": [('CONTENT-LENGTH', '24')],\n    \"body\": b\"GET /second HTTP/1.1\\r\\n\\r\\n\"\n}\n"
  },
  {
    "path": "tests/requests/valid/021.http",
    "content": "GET /first HTTP/1.1\\r\\n\nConnection: Close\\r\\n\n\\r\\n\nGET /second HTTP/1.1\\r\\n\n\\r\\n"
  },
  {
    "path": "tests/requests/valid/021.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nrequest = {\n    \"method\": \"GET\",\n    \"uri\": uri(\"/first\"),\n    \"version\": (1, 1),\n    \"headers\": [(\"CONNECTION\", \"Close\")],\n    \"body\": b\"\"\n}\n"
  },
  {
    "path": "tests/requests/valid/022.http",
    "content": "GET /first HTTP/1.0\\r\\n\nConnection: Keep-Alive\\r\\n\n\\r\\n\nGET /second HTTP/1.1\\r\\n\n\\r\\n"
  },
  {
    "path": "tests/requests/valid/022.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nreq1 = {\n    \"method\": \"GET\",\n    \"uri\": uri(\"/first\"),\n    \"version\": (1, 0),\n    \"headers\": [(\"CONNECTION\", \"Keep-Alive\")],\n    \"body\": b\"\"\n}\n\nreq2 = {\n    \"method\": \"GET\",\n    \"uri\": uri(\"/second\"),\n    \"version\": (1, 1),\n    \"headers\": [],\n    \"body\": b\"\"\n}\n\nrequest = [req1, req2]\n"
  },
  {
    "path": "tests/requests/valid/023.http",
    "content": "POST /two_chunks_mult_zero_end HTTP/1.1\\r\\n\nTransfer-Encoding: chunked\\r\\n\n\\r\\n\n5\\r\\n\nhello\\r\\n\n6\\r\\n\n world\\r\\n\n000\\r\\n\n\\r\\n\nGET /second HTTP/1.1\\r\\n\n\\r\\n"
  },
  {
    "path": "tests/requests/valid/023.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nreq1 = {\n    \"method\": \"POST\",\n    \"uri\": uri(\"/two_chunks_mult_zero_end\"),\n    \"version\": (1, 1),\n    \"headers\": [\n        (\"TRANSFER-ENCODING\", \"chunked\")\n    ],\n    \"body\": b\"hello world\"\n}\n\nreq2 = {\n    \"method\": \"GET\",\n    \"uri\": uri(\"/second\"),\n    \"version\": (1, 1),\n    \"headers\": [],\n    \"body\": b\"\"\n}\n\nrequest = [req1, req2]\n"
  },
  {
    "path": "tests/requests/valid/024.http",
    "content": "PUT /q=08aP8931Ltyl9nqyJvjMaRCOgDV3uONtAdHABjoZUG6KAP6h3Vh97O3GJjjovXYgNdrhxc7TriXoAmeehZMJx88EyhcPXO0f09Nvd128SZnxZ2r5jFDELkn26reKRysODSLBZLfjU3vxLzLXKWeFOFJKcZYRH9V7hC98DDS4ZsS7weUksBuK6m86aLNHHHB0Xbyxv1TiDbOWYIzKxV0eZKyk0CaDLDiR0CRuMOf4rwBeuHoMrumzafrFI5iL72ANQZmOvKdk1qQeXkRqEG11YU0kF7f1hSlmgiIgg5maWiBsA9sAg36IIXZMWwJF63zpMgAyjTT8l4pQhSBfhY2xbGAWmLGpyd1rlBm0O5LCoKpnQuTACm2azi0x6a1Qbry9flQBO4jHge2dXiD1si6Gh5q8fZu8ZQ7LLWii2u4rGB7E4XlhnClrCHg5vJmjYf2AItYPA0ogsiIdEEQGpzMJPqrp8Icn5kAAimWF1aCYaDjcdSgWI48PnoxlzIHX50EPFcPOSLecjkstD9z66H554sUXfWn3Mk9lnOUlse6nx0u1YClFK4UFXp98ru9eBBr7pkAsfZ34yPskayGyXPPyzWyBfVd28UuvdEG47SMdyqEpX0rFdk67fAYij0PWMK79mDmGAS37O821o18XUbu0GQjsqAGVMN9LDIAliD9QqtlwdEnplKkUyyZ7GAFJCFffgzppU9CjA2FbPX6ZjTOi4sPoYEyhyeQKVqAe9keYeDpU2qDwq83XEDQUKvP0w48GyavSmdBcrMXjUsu0PfdYpSaKwarrUB3i93HgoQB3ZJIR4lW6iPRTmm28OEKq2MIJGAoTXxCZYM5UacRldlqQOj6JkYz6y7ppWOjJ9yiCUEenuvfcItgmw9HIgGA59JxO8NDLEZLSONfuIgiV7wjsJnxuTOlU4vkjV7fTuOeU91xez7UKhaTqqEW3XBUSLjhKi3IkZg7ukrGZTWPhijFv2EZwEWDAyLlHvZB4X738zGJUlEX1k52EHwrKVKdLfePcaOjAGKsongHBFYxYC8vBBLuKm9RWexKCT14M25pCGloJXZ4OpBRfDQA2kobLUcEXEpzqRBPGN2JdNSBOFlUtUxWKnnPBM6r9S356l3k1o9zTIPeoIitWRjASs4A0iwYc8p5vv5Kt8KtsmW7Xv8dlU8HbZHsy3LI7O9BpUH8cJubqdEhooKABkx71pdcsZGhZb6epyTiPyvOhdJ7tNtFy3KQOameqTgGyd53Z42eZ0AjaOEvnzermi2E0xo3MMHFhB74TFtNAI3ppxxyqknc1mzUqZ49Wi8YPBg9ids6IgZvddBQYvwEozkmyGAkatQtt9TD4LjU3TyyUlhNG21q7CzEEl8NNsVrV6QyHsfw7E5w7XcoT7OQkBYoZwHIAjfekehnpc2llRtRY5m43fPVasmsVazOR36DRSLZJPHAqUDO0LInu9mgP57Mnz9CgylEmdE2aaYs426rnTFR3G3CfjLofHfjaLOkAegr4W3jx6MNMMOMZw2u46YTCnlfbBK6ZA1UYeAH1DIQJykcSQESinC8HpYIJt9A8g7UT0awzRP1F9nHa3wDnaAHndQYKMrjzlWo8ejQ0XHWgHhqnWHgW4h9sOnJckH00CYK1fHUKASJ3D8kOKax6uplexfz6BCvAoL9zm5TjeB1yxrpLp9NjjTWSKG2HOZhPkGpdEqU4mjnN2AkUVACPGos5YLBmTnSrdOEGZJDlAvJOUt800Mu3BYc1MiDIB6LMSSV5RsIUDFOzNletGQoq4G3yHZmx78uEse5vUTPFF3KT8LCrssqdIU9H97Npgf6N5j8arQ7ykLzN459jJaUzpGIo6uowPnUSatDf9GAvAmWNvsVTz6bYiAV71C7QF0C7UolYIQY6DHJEHejgX2YMEovWNLPL50eeC51h4DdPNv5G4ZdNtQTRVybYBZMpetGDiFmXN0JKa1sKHOSZxdrhKjxDIhrYVyCcRUMQ0sjGGHFuOcRszr6E5igEMtsebHQ3KYiGd5B27LikpUHhk61rgZlulHdMoS6YgQs6SV6UMVNku6sCw529xhUciDwRMhsbAjDlahYbrGa3NryxyV5LrXONGGKCchCqv7vDMdAtPrVr8M2vL5MySQAC3g90iugGQcLH3hCf9f1Kn5X0hM4KZTfwOPJhlfJsMRNhssiDoXaycUvOUS58266yPDlitPIAzO03XClm4EDPXGIwcwiFr7FcDo3tQIMZVy87i48Zb80s3zAYRiBIS0vO3RKGx3OGN5zid2B7MfnfLzvpvgZoirHhAqXffnym5abpZNzGuo5GowTRA2Ptk4Ve2JFoHACWpD6HiGnRZ9QVOmPICoQrSUQw45Jlk9onKJz5Erhnx0943Uno6tMJ5jbrWBNiIO7i04xzRBgujeiAJvuQkVDX2QLKRxZ7s6rhdfOaq6R6uL108gEzzlXOLqTTJXgM63rcUWNbE7wsIXcCFSF59LLJ7G5Qea33suxdDX6DcK4a0VMZoxmWPtCi1dAT9ggJqc2Sh7mkAqizaB16RXZvSydchpdVj6s4qn4ivr0HKHdAstX0XZ0FFU6lOiNmU3vasMg2uaVG8tyuG8N8VsuXIOQs7xtFxDhilYb8MQ9vES9pWfWPSXFlJAq4XKPY8a0JOIx57EQuWHo3uWgRTIRThvZP9YYzSnjGIHwjS8JeppICHofADXZhJ0uDQaQs7MiXEALpGmT3W6w0G3tBdZcuTDkWx1HsT5jd9jQeJpgD2VxdKh8U4Q3vANTAuwBXLJ2P0stS8Q72JWgNPwKYTY9cPoaGZlUFGgVsq8CdEFH9yW0c27G5s5sfHsyep6t4VxIHHMOX2GmMRyGxDI33am1J7ZmJ1NyXiwkHxtPH5QBpU2PMu2Guf3xIxlk3snMkMAsGO0vYfqO9tdIgdxMYO3HZTYv99OXaHcNQ5u0pRZZyVrNOIPurkEOdJy0nowPemIgUuHWh8vQCuDZav1m35AOl6ftSFuChSm5KstEWnC7q8mJ0juJEBkCRmQphP3V1pqiDjz6YA90qEe7MA3nzT0nHG8A1hWlqcPVPNz4qWNF6Fq1ub4075aXO0H7Krb6rhWGb3ZRPjpb4BKN8jGFQrBUMZprtjAJ67BnfmYgE0mmGLV2QP10gYS1T06kBRyrtp7he6wsPiBPJ7wxPLHNUN2SGQHBTSKagndM99fuaga5Sw9OT8Fzdo7xUJXfhJ97gUnNDrknal0B00NMNvajZeQQTJyBsVSwBZtZ45ZCcq1idc7GWC0MITSk58cIVkSPXbrERUaygyY13dPeEVzjVi9aVJwUF6eJu1s8u3FCJqp2GoWIItwvZO69asX75fekFkmFpNavxM0X0dZC01TTPpV6E6PJoIfW8C06CKNHV7Gk2mkTWGSwUG4xD2L3G3XarodHDcmumFJX9Xviv0rvm38SCtin6OpjH8MHYDrj1OxTJbC2VclJxv73z2BDBquosKOik0fmgbPZN0FUTmjBEwHTvqd5QHTwb3nOpEz3X6YCF0lrcrQc0uhyr7gBGBs86nUBWFRp1LKjIRVTVXDipajqNDTQGNZtzvR9MUf1yJJV07inbrlPOENd7rHpKCrJtoZXOkDqInaIqoMCG3DVd353BGmZNJEKOa3DnL7fb9zwuHlvHAfCco7ZS4wAV87trWkp6skXux9v5WhkumbUyGq4ia6DM1PuqqnFfBTAWDzJsnggAJrzr8O7JbDtaXwcW9sqaOb0S6NvnUDZqiNdDQPMDOKvXRJJJQdf1FSrPCCSPEEWO1SeVwictj7rTbpWGRoukwhgJALys95pGGOQxCPzRGrtVFnGcsLN1CwI3wLbmDnNKUv3KpOLEOPRxQXeXuJRIiYCFum44c0wNr731DvHn3YEJMH4iwFONl1rolEL4w6KFUOCq7ekrE5iyUt1V32PNtuUshXRjOYjBval29JMH5GoqZlGhCczzHMA61cmuzqdFwiPCB9yzqvJTg8TqMNvwKJztFIQK4mc5Ev5rRVSozD796AVRKT8rZF39IA1kmCLdXqz7CCC8x4QjjDpxjKCXP5HkWf9mp2FNBjE3pAeaEc6Vk2ENLlW8WVCe08aP8931Ltyl9nqyJvjMaRCOgDV3uONtAdHABjoZUG6KAP6h3Vh97O3GJjjovXYgNdrhxc7TriXoAmeehZMJx88EyhcPXO0f09Nvd128SZnxZ2r5jFDELkn26reKRysODSLBZLfjU3vxLzLXKWeFOFJKcZYRH9V7hC98DDS4ZsS7weUksBuK6m86aLNHHHB0Xbyxv1TiDbOWYIzKxV0eZKyk0CaDLDiR0CRuMOf4rwBeuHoMrumzafrFI5iL72ANQZmOvKdk1qQeXkRqEG11YU0kF7f1hSlmgiIgg5maWiBsA9sAg36IIXZMWwJF63zpMgAyjTT8l4pQhSBfhY2xbGAWmLGpyd1rlBm0O5LCoKpnQuTACm2azi0x6a1Qbry9flQBO4jHge2dXiD1si6Gh5q8fZu8ZQ7LLWii2u4rGB7E4XlhnClrCHg5vJmjYf2AItYPA0ogsiIdEEQGpzMJPqrp8Icn5kAAimWF1aCYaDjcdSgWI48PnoxlzIHX50EPFcPOSLecjkstD9z66H554sUXfWn3Mk9lnOUlse6nx0u1YClFK4UFXp98ru9eBBr7pkAsfZ34yPskayGyXPPyzWyBfVd28UuvdEG47SMdyqEpX0rFdk67fAYij0PWMK79mDmGAS37O821o18XUbu0GQjsqAGVMN9LDIAliD9QqtlwdEnplKkUyyZ7GAFJCFffgzppU9CjA2FbPX6ZjTOi4sPoYEyhyeQKVqAe9keYeDpU2qDwq83XEDQUKvP0w48GyavSmdBcrMXjUsu0PfdYpSaKwarrUB3i93HgoQB3ZJIR4lW6iPRTmm28OEKq2MIJGAoTXxCZYM5UacRldlqQOj6JkYz6y7ppWOjJ9yiCUEenuvfcItgmw9HIgGA59JxO8NDLEZLSONfuIgiV7wjsJnxuTOlU4vkjV7fTuOeU91xez7UKhaTqqEW3XBUSLjhKi3IkZg7ukrGZTWPhijFv2EZwEWDAyLlHvZB4X738zGJUlEX1k52EHwrKVKdLfePcaOjAGKsongHBFYxYC8vBBLuKm9RWexKCT14M25pCGloJXZ4OpBRfDQA2kobLUcEXEpzqRBPGN2JdNSBOFlUtUxWKnnPBM6r9S356l3k1o9zTIPeoIitWRjASs4A0iwYc8p5vv5Kt8KtsmW7Xv8dlU8HbZHsy3LI7O9BpUH8cJubqdEhooKABkx71pdcsZGhZb6epyTiPyvOhdJ7tNtFy3KQOameqTgGyd53Z42eZ0AjaOEvnzermi2E0xo3MMHFhB74TFtNAI3ppxxyqknc1mzUqZ49Wi8YPBg9ids6IgZvddBQYvwEozkmyGAkatQtt9TD4LjU3TyyUlhNG21q7CzEEl8NNsVrV6QyHsfw7E5w7XcoT7OQkBYoZwHIAjfekehnpc2llRtRY5m43fPVasmsVazOR36DRSLZJPHAqUDO0LInu9mgP57Mnz9CgylEmdE2aaYs426rnTFR3G3CfjLofHfjaLOkAegr4W3jx6MNMMOMZw2u46YTCnlfbBK6ZA1UYeAH1DIQJykcSQESinC8HpYIJt9A8g7UT0awzRP1F9nHa3wDnaAHndQYKMrjzlWo8ejQ0XHWgHhqnWHgW4h9sOnJckH00CYK1fHUKASJ3D8kOKax6uplexfz6BCvAoL9zm5TjeB1yxrpLp9NjjTWSKG2HOZhPkGpdEqU4mjnN2AkUVACPGos5YLBmTnSrdOEGZJDlAvJOUt800Mu3BYc1MiDIB6LMSSV5RsIUDFOzNletGQoq4G3yHZmx78uEse5vUTPFF3KT8LCrssqdIU9H97Npgf6N5j8arQ7ykLzN459jJaUzpGIo6uowPnUSatDf9GAvAmWNvsVTz6bYiAV71C7QF0C7UolYIQY6DHJEHejgX2YMEovWNLPL50eeC51h4DdPNv5G4ZdNtQTRVybYBZMpetGDiFmXN0JKa1sKHOSZxdrhKjxDIhrYVyCcRUMQ0sjGGHFuOcRszr6E5igEMtsebHQ3KYiGd5B27LikpUHhk61rgZlulHdMoS6YgQs6SV6UMVNku6sCw529xhUciDwRMhsbAjDlahYbrGa3NryxyV5LrXONGGKCchCqv7vDMdAtPrVr8M2vL5MySQAC3g90iugGQcLH3hCf9f1Kn5X0hM4KZTfwOPJhlfJsMRNhssiDoXaycUvOUS58266yPDlitPIAzO03XClm4EDPXGIwcwiFr7FcDo3tQIMZVy87i48Zb80s3zAYRiBIS0vO3RKGx3OGN5zid2B7MfnfLzvpvgZoirHhAqXffnym5abpZNzGuo5GowTRA2Ptk4Ve2JFoHACWpD6HiGnRZ9QVOmPICoQrSUQw45Jlk9onKJz5Erhnx0943Uno6tMJ5jbrWBNiIO7i04xzRBgujeiAJvuQkVDX2QLKRxZ7s6rhdfOaq6R6uL108gEzzlXOLqTTJXgM63rcUWNbE7wsIXcCFSF59LLJ7G5Qea33suxdDX6DcK4a0VMZoxmWPtCi1dAT9ggJqc2Sh7mkAqizaB16RXZvSydchpdVj6s4qn4ivr0HKHdAstX0XZ0FFU6lOiNmU3vasMg2uaVG8tyuG8N8VsuXIOQs7xtFxDhilYb8MQ9vES9pWfWPSXFlJAq4XKPY8a0JOIx57EQuWHo3uWgRTIRThvZP9YYzSnjGIHwjS8JeppICHofADXZhJ0uDQaQs7MiXEALpGmT3W6w0G3tBdZcuTDkWx1HsT5jd9jQeJpgD2VxdKh8U4Q3vANTAuwBXLJ2P0stS8Q72JWgNPwKYTY9cPoaGZlUFGgVsq8CdEFH9yW0c27G5s5sfHsyep6t4VxIHHMOX2GmMRyGxDI33am1J7ZmJ1NyXiwkHxtPH5QBpU2PMu2Guf3xIxlk3snMkMAsGO0vYfqO9tdIgdxMYO3HZTYv99OXaHcNQ5u0pRZZyVrNOIPurkEOdJy0nowPemIgUuHWh8vQCuDZav1m35AOl6ftSFuChSm5KstEWnC7q8mJ0juJEBkCRmQphP3V1pqiDjz6YA90qEe7MA3nzT0nHG8A1hWlqcPVPNz4qWNF6Fq1ub4075aXO0H7Krb6rhWGb3ZRPjpb4BKN8jGFQrBUMZprtjAJ67BnfmYgE0mmGLV2QP10gYS1T06kBRyrtp7he6wsPiBPJ7wxPLHNUN2SGQHBTSKagndM99fuaga5Sw9OT8Fzdo7xUJXfhJ97gUnNDrknal0B00NMNvajZeQQTJyBsVSwBZtZ45ZCcq1idc7GWC0MITSk58cIVkSPXbrERUaygyY13dPeEVzjVi9aVJwUF6eJu1s8u3FCJqp2GoWIItwvZO69asX75fekFkmFpNavxM0X0dZC01TTPpV6E6PJoIfW8C06CKNHV7Gk2mkTWGSwUG4xD2L3G3XarodHDcmumFJX9Xviv0rvm38SCtin6OpjH8MHYDrj1OxTJbC2VclJxv73z2BDBquosKOik0fmgbPZN0FUTmjBEwHTvqd5QHTwb3nOpEz3X6YCF0lrcrQc0uhyr7gBGBs86nUBWFRp1LKjIRVTVXDipajqNDTQGNZtzvR9MUf1yJJV07inbrlPOENd7rHpKCrJtoZXOkDqInaIqoMCG3DVd353BGmZNJEKOa3DnL7fb9zwuHlvHAfCco7ZS4wAV87trWkp6skXux9v5WhkumbUyGq4ia6DM1PuqqnFfBTAWDzJsnggAJrzr8O7JbDtaXwcW9sqaOb0S6NvnUDZqiNdDQPMDOKvXRJJJQdf1FSrPCCSPEEWO1SeVwictj7rTbpWGRoukwhgJALys95pGGOQxCPzRGrtVFnGcsLN1CwI3wLbmDnNKUv3KpOLEOPRxQXeXuJRIiYCFum44c0wNr731DvHn3YEJMH4iwFONl1rolEL4w6KFUOCq7ekrE5iyUt1V32PNtuUshXRjOYjBval29JMH5GoqZlGhCczzHMA61cmuzqdFwiPCB9yzqvJTg8TqMNvwKJztFIQK4mc5Ev5rRVSozD796AVRKT8rZF39IA1kmCLdXqz7CCC8x4QjjDpxjKCXP5HkWf9mp2FNjE62a HTTP/1.0\\r\\n\nSomeheader: 0X0VfvRJPKiUBYDUS0Vbdm9Rv6pQ1giLdvXeG1SbOwwEjzKceTxd5RKlt9KHVdQkZPqnZ3jLsuj67otzLqX0Q1dY1EsBI1InsyGc2Dxdr5o7W5DsBGYV0SDMyta3V9bmBJXJQ6g8R9qPtNrED4eIPvVmFY7aokhFb4TILl5UnL8qI6qqiyniYDaPVMxDlZaoCNkDbukO34fOUJD6ZN541qmjWEq1rvtAYDI77mkzWSx5zOkYd62RFmY7YKrQC5gtIVq8SBLp09Ao53S3895ABRcxjrg99lfbgLQFYwbM4FQ6ab1Ll2uybZyEU8MHPt5Czst0cRsoG819SBphxygWcCNwB93KGLi1K9eiCuAgx6Ove165KObLrvfA1rDI5hiv83Gql0UohgKtHeRmtqM0McnCO1VWAnFxpi1hxIAlBrR4w35EcaryGEKKcL34QyzD1zlF4mkQkr1EAOTgIMKoLipGUgykz7UFN1cCuWyo3CkdZvukBS3IGtEfxFuFCcnp70WTIjZxXxU4owMbWW1ER5Gsx0ilET0mzekZL0ngCikNP2BRQikRdlVBQ3eiLzDjq27UAm7ufQ9MJla8Yxd6Ea37un9DMltQwGmnmeG5pET54STq72qfY4HCerWHbCX1qwHTErMfEfIWcYldDfytUTOj7NcWRga3xW7JYpPZHdlkb24evup3lI4arY6j5a12ZcX9zVI02IJG0QD9T4zSHEV0pdVFZ8xwOlSWKuZ9VZMmRyOwmfhIPA7fDV5SP8weRlSnSCSN4YBAfzFVNfPTyeoSfVpXsxIABhXEQTg12YvAAn9390wFhEhMsT9FWIiIs7oH63tQyjdEAZSJcZ0nSQfapvi4BDsQSMv3W2DofSzxwOPrVQWRMyvP0UV0J660Gc4iZ2Tixe3DSeqg9VuNvij09aCbkBdwJh9r4UWmM1Hp1ZDF5Rr14nKtFAgjVlGlfZi4bWQKTzOlqaVbWBvxdKsJ27eelyDnasIPqo17yY5lg10Lb8nyu60Wn7l7Xb0Ndp334B5am4Vh1foctvkkhNFeIejtnjPYmWjS77rJ1aL0zJka4Xog5Oparvc93Pddf9CzCxgle00BTKNj0syVo5uqvX5PVzdhAnigU4jdPbJbcPpbpJRU4UDqIswRNJOlGfpdLnCvnPIRB2a7btjFTaE0tne0TjedGbePje1Li21rPXPX7t5LICWl1SRyqQ9x9woGEv1sI5VgpRoKtS6oxWgMERjP3LcEez3XqLiSwv0rWMlDiJhxEopz8Mklx8ZygQLiwIYx2pNq0JhKB8K1lZ8dYE5d3nRWhXwG4gFTUg2JYjnjL81WGRmjXnZEVLwYfYBUkRlqWAYHi1E6wF85BfcwvkgnEeBTiQSlfu6xwCYaW2OEogq7tbdinvlpeEPij1qQivpcs573HPHpkXrEeXC9P2gZhmV1Rvn69NAN2lOXSVe8XotSyCG5fHFsTDYlOvYW8EBrAdWuZrwU753xwjk3QCp2ODetYze98voig4lfYHrrWT43VXcHt8J5z7U3kt5O460buwESBhgkALZdrFYyy4YQcmnAeSCw5OoLArDEmzaI4JkFBCDqQxTE9BTYA112r9ymuOo5MGkTDYZlvtvopG4ekorfLoIa13Z9L6ZilXT1cg55dvNlOrbTSHpQTYRJfJ6x71IpDFyvdbZbOHQYMm98fcN9CLqFErkpcN4JO26GIhSodGGTSnzyUxBYueawFNlGxCMTa6JseX9c7Xlo8NRaZHBPvG7Z4gUCkOdUSEW0RRTs3TSSdjEKnJ6u9RdDqqyvN8cJ7gliTd04mSyVnkmxdqVU8DrdIrkSCfVQNoFgdydDHS3wMLU6QGTGBzK5pd9EfsDEeYXtIb3CkRupM4SERGMTN8TyIxqqIyWmgjBmSGLTFOB5tsPhkVydVQNf7jBkDy6THfBy0uALVUkm2jLeTFXjajyeL4ms5Lgx0eLoz0XWN6WulXSA20zV3ObSCHbBeVUgKmPxHq5qPmAi04VFIvCOJ0rBQJh9ZHJMwvhI3VEBF6EmXOiRCn0XOhm3pfHlmaCAWrOSGuQs3NCNlFRjwmVRPY5FJrKYjH3FrLrLdU07zdViAix8C4LxVrRrMB6ligZC3CoDhFA4vMjiPU5SBRqRW4lwVnvMZEZbf0AYbBc2ymnKAOWbQwt2ldiI2qL0aLoL6YtSFUhpwMOR3LP1feUq6XRO5xc9V02nEt9MRQsl5MgmKMcXap4HqAN0yATpjAGRnWqEnE7E1XZg95cEl2gO4HXejKzR0kiTUudcw6P4t1RYLRx7isZNJxiq1JZz6FpEe7QhwGbhPySNMbXJtmYuhAaTpfGdGKMxvHHB9LmELOChdyfjHMwMZ2B0xgU2eJgJimCwLH3UEmExgAwJDD4GSCqevYAMK4P9FKPl0dku0KZ7uOJ8oNloEsrbvMuhuKFDuO1PNvxtdCcgASzNVzdueOtUm1giZIDqbb6j11nqi9NoFeck1zZi2kfGF7OeUp4vYszuhQNi4vd03QeVAduM9h9v36Nz1YobRxB2CjTp6qdKdW9IYBp8aExZpipnJIbfD2hTWE44kIu7Q17f4C9kycGjsLwAWkVbfTRmBMU8SbVKV1EJTrN1gGqGX7quSwg1Vp4qslKAk6EIkoReIl5DuzuH8Rbvrkp5LFFAhNhb1hvXvVWcibtDjQSradNtuYzGf2AAduhxOTnZjzbsceGYhQA5a5NtqxE2GBlW8CPoPzIyfMfPjdAIUmAcns7Fkp44nju2htwhryUyidEzDVyTwevquARjt5a7eu8qIKfPrYgbOAlPgA1JHNi55ivTNpDuQ8drNiafZIntA43HI447WtITYYvLxFRG8OWvJRwI0N7dvHYO8H8lYI1OwatfvLKlJqjtdJBBvMWXdT4SbxHUdNTDUQmqFGZaLx1AvYPnJTYRzrqn5ZnXyWQ1ZCwtvZK209TxoezJ2sGorE46C7Zyki6EcXlX2A8upUUh9IhqLYTzidIRrAPE5mZmosyDyShjnRiN5CLXZAI21eV4v3a6WXI8TKkUk3fhhajOgPXshlyCEfDAyESpz1J8RECu6vQs81E1ZNE5ha5UGw2wk3Ea8oSTfqTiu0OeisV2a6bfldvW4x0OL8PS57uuY0v0OZPSUPWmPQgnmJRVw8vmh62bpFekMnUH7y31fXU6MIyZaiBs1FEu7qF6irBszHt2ARy50SjgGwQZWcecgvB8gB874g3ES9mZer3diYGF3Wssmsm6XRdsNcuNn3yzuoi52cRrBYUOISegTBVApn4zfuCC9Y4AAfe6wmmiuN8hL6KJeOjrdK5EFQHGyrzeuIMaT3B2nKz1PNONVQ0udbqCQebz3cq7NPe6kGKFLiE6euWjdoMuAbuu8rTkAa42ensXz4a1Yo450ZVgYypaDtepDQWFkJyTHDW1HTVZfCok0tp7STRiQ8n3NKxOUSL9veuTsDs1FaV2rbzR3DvkEJrhJ10Rm0pvLgui5GUDKyWLnrqcNVtOIzFaj9K5pwMfnREm1VIs84ePX0GsMjirfOfubzDoYjavbiCtTB86nKx0tfCKtl0yUQ5PWSBqdGASY3mr5hZcFZ9bA6uXXGTNqMpUH3gqxCoF6t2yAim93t77jYkiFt3OBlBRVQzRsPbgEKRXbX3bWQj6NpDzNCQPYTs45HsQB967f4yByzLH8X289YAZJhJJyFTMCLbpdKFuMBX5Msyr4d15sBa1h5bI13dqU14WBnMKD12LkHMjHiyde6xf5EELf082sUfiAZaROFuDCDnA89p6y6oYEUgF1L9yQElZO4R6IrkJsEFN9hvARf3CH4ENqbYxtUN9gsB9CLCGKMy2R4wGKU3Dkyea27YCR4QHCdqX3HqOpy12uxBANvbrfEro9q5NJrGK7WVq3nNabN05x4TmIZk3asc8ehvDyhSgQLY0wwyvrkcYqNiETybJ57RjwVg1YE0IZEBfyAUNXE4goc2jtbZbHfcpTzt08pSJQZTAzuxrdQLS4EnaFHPpMdPh1YXUdclj6g2sjYbhoTYcV97bVDAUztMZ4EarUcv6tgQOvK66RmJCF2zVEpFDBS6AVZJWzrVlnuiweXpH0L9eY2Wy2EuAHi7gL4o0i0AkOapqY1TPUWUwBaVrKQzkL8QQbczgc97pMvSnGYMlcSdzlamFtUmRoOPmhBGMpVqmcxnstnqJ0TXMV65zbRN2hk3YVF5HwPjuWJmfkVYnyazuqKuaaohrQIe7YOOSAmD7C2vDnI50y1oScQqIPb87QAmguFz7jfNBSPymjPJ7UrToaJen7LEQr8S2b69ayZYNIyWbcpaW5ACUqdyT5AeHYhdENORnWS2B17qnBPtyvb4WujJCafLmsMFhQbcGonDZkHEOAnOcwRwJ4KIPr4MlQLRKsdnurPDDEmpCtCnFg8vPObOPHoHgICb9j35pG1YNhAAGIGTZ4g3JTJzFvTcW7GDRxREPZffKOuQTJoMYYaaPwnE0SainEpCFAukJbDy1ss5cZt60nqTw1asLzwMKJu5PHpU9sB9YN7J2cPhIbfb4387zSmSvqbt3I8NFjDbuYEhe6nZ7gRT5Th0W0MoyzHlmy4MSXbaAfUJNsLQJmdhdVKDsqMz0aXKIVNsXtn88owrhw0yqxU0K3IfTothafhpQ8daRUnbjzULViWRvUz7dI1N3GgylRzaEXQPgbj0DQ7RujNTcJoSp7I1ELjFFSBZDm4Jx5eXq0aS2SKJPFX7XmFfkkR99wRiHx4ByVTL5umojRhY5j8vg3l3yfliJbeOTXckaYiezrucuHaiVFWR2kjk9PUm57bDpvtSFMic652iDufj4hqpy5MH5r2lg67T6Bbb3fcq49cVJ3hkN2GfRqVhoPxmHyvotu5koheVh7oHDaLaf4VvcQMd5MF8sicaX3GXfoLjlfFZwfJBpXNbbVemD7XghpIEwuFjA1USU8yJnTdvCJ2bFmPNWFeWsBVDyl7XUsbgB3K2zz806xODZT639dqiqhGXQNbgYtShikQhiHhZF4wf4IY588LE4EO2bdXBb2Wezm8Gl2J5GAfqnx5Z6NF7h1gGkM27hpnmKNylKZjqTNANj0CRU4awpdVrYGX7hT0u452Y5bXpVl7cLuK7j2k7VG93NXPsXADhQA8R9WDcpU0PLzFWFq1omoQ9ZRSlvh8R4pRp4vHIYf4A5uQEmv5Owr4pFQcWdp5GAdkpBaSHvUhvMxOSpsqVB2LHvvs1RiOUHHhHdZEKpX25mK9moud8pKT4efru1SlRRSsxdz87hTJMUrueydHDPXbo9AvExctdqxuCk03Fy8cB57qrkQQ50oGNuTNPColMrwVfmuTt81uSZremLbINILnCVXEnvTugRQfFYMnprqMB4mVJfZfh6XVLdOyW4BPaFrBsZGFy7udoWJwE8ACx4UpJW6m1ltckofzA6AUxzXprXDCCL118m8bBB2hzDKmqeLk5ZYKsLROkTqRAxmJjBSZSo2XBroO5rVvkOZrOZRe8NgaHFMLPn0I6hsqwA7VdKlpbqknax84iWrtBe8ErxgPIQeYhELyK1deW1YWBagD21MBTc2h5LliIlglZg41H8Zl3GvUv0XNZegR5bx1kiM9WFGV9Yt37iQQGquWAMKCAb6AqpkCtKs7sXKaEAVsbh32tlkAg4ngspjwzYHTPYKUuigPX5K8siUfaAW9WJl7r8dc4ju97osWETOcBENLsfwB66TvsttORtOedylnErplZP3hjt7o39JllXDobj3l10bSr4B09eYVWi2DLGavYktKSKj1PrqzuGUaqcFxqoebpuDEAx5vl8ZmSYrmS2RBJ1n2s3lkKdaVWTmfIXlyMMT7Ac3lCXpGNnpf8ccTffv3E0fBrpCSpVc48dM5e5iTpRPrfWxAjrud9jSrqVBXsw3pqUvhuVmBpmwoKAfQGxHrauna3f48AFefGDozxXXjpdM9ZDWHsRUBTFNzDs8tUATtegSzZfNJCS9k0p5q2cueyU1mtwMJIdf0FrsVGiAyX7PFkWvLHi29fpprZQd0gbMMw2Bt10ZbZCsjPX261cXmVa6ZPnkVQm2w1ory3uWejuq20oQCyXTYyv1Ki4tbdPxoNn04Je7uS3QHDCsUl4i9zKNhBJ3g55bhIZWfwmLi3S7oY16gImdC6vvjsMKkCPzXv4pPaVhHH7o4f0mWEz30k4o7GQNOUy8LPM3NmlZF7QaIBdRfozG86jwQkC3jTNR357pdPjOqMERtIS4WEJBgbaeUCu5MOhsNdaD91iCeghIpOECFyTdEkUCGPPCIAtuAOKBdhPu40UxHx30dELMTK3azHOuOnLTsdiM4KJ9yF4Ab2eiz5j2T95sDx3aiEJDVDPCa55hO0XTBM9OSNtdzjdTdZT19XrwD0wPWZcBhfJ66X1uNM2eud1btzglqZP52qqYU7BK2M3BBZKKjy7P6YzmgaPHWnFGHZdwdz3Yq6e3N76Cjkfl8Sy0mkwd6pt0geDM1jNNZrcT8dUfLLaiUqcZm1KRVdpZaBrboDSuCxfWYlxqgsldwlGL4C06ceFUDXX8PzxzWEgOd8OU4F22pcNJOnwJGo6rYA3tvhAuq2WKVg6tgFCb1p7dzF4Ke3J0dv3IneMSNnHG4hkvxW6VzIykDUtYEjMQO35tdnEA0vMVLXIahpJpz4HGs5wwRgoZx1e1zD1pXi7KmEVTlfattgcGFlKjZJ60fEdloZEmiXodxT63CzuJHnjHDOL8qcMzTxHb8OCainga4w1fk4uILLAWqmTFpDcFGSF5lbOFUwhvtMK6knIWZ8ZApZvTGBt1qv3xKUJqPcWiweI4kk57zgyTPZku2mg4fJWDKSfiRSi7LvtpKkdqjein9lP7LMv5lKutprVzjmvHBPjunXGqakWx39xYH8RD6qF3Fw2BnIIesiicZsDv69Ggbu9Y334UeFPNIJ3LGp2I8xcUxlP5dJAh4V05p1HvIZ5Fhk0oCWlvNXdLqzbVsbfW9jWyQTaZXzw7WT3rqFQc7wvw4ayp5eKmUclqB1yOvrI14XGhmH7QMaAYNTIE2RHjYXVgvbmFRi0oB1v4nDEeSTn3KHBRQD8TilCagKg0XYPj2eAgWs12ZRYzlGyCvYZ1pol5wAwc9AFFGwsTJ9UYkbxlZv7wKDx7nFzlUSMC1kMvS2ECwvHzSycqHPRwCGipvG6kWz0mGvASXeKjm47iMROoY0MRK0uvgNdTTOTdxkMgOuCDIlxfit5QKjyzaVAg2kDwENfSd6XPMgSprTSLuNDXdg5NHCwUvDbEHVxpMgOItZymPZtPweOrnPdlEB4UwLZ8jqtShi5oDYvhkh85FwwT25OHFvDUWTTCV5n73pQ8kLo8zsB3mbWfGwg62guj3C50Dh42fAZEPBRSHDRTg3r0z39Vyj490lk2UpZeNyylwuEKmuIqEkbE3BRT2YEjTM8a2PU5grCuzculibcoRUpb1sIQiMRTf4wrtT1CnKcoUJ1T28DC04dTJVRcm3w3WzNLdrnovkX6NahblTzDvq5eXkoEaZv6HClmGuho4FH6s6i0OdmmW8qkNOnk7BhexiyAd3UYERlFwvZ6LP55tFOc3vnlhyylx1rTTgu1NFljRNs7rGiT7SnGFaFK7GITEZFEYI7DmOEUZXxDSHjYuOVN0YAJP2cZFgagyMwGJdrpH8S7cewYPMKz2Go2GBKl1OA6pJ8T91tUdEcGVg9JCMQUA4sBtlIuRTVV3cduIhsLCTi2ewItkh9MRP1kevVa9WcXejQQKreZmq5EZtzThW71r7E2tcvwFeqiwv3JZnV16bZ7NwZT6uvSrOnIFUyMsxhh8xCkVY82VLTAZhPXB8t6CbyjZ5stos6WmNZgoEsD8GU8pmzSTubAqQXkTbiODF2pePe6S9uQ9HngGGBnOjY4QUcAcScDsfflyXVqyxgTelGD4vXoba6qRWCqc9LKpyk4jCKYvLX9tzXusO7bhT2KRvF4MObDqdE4KnCCIF3zeVD0vImR20MmRTBHRCNm3s6GfyeTYEAlW3L2igZJ7Myj5zGLccMt2EohGc38HfWZ4mlvXRLHKB233PyKALYifqlAxTXaWUk13o6nACQDvN7DxSCA0daJeuznK1Dr52bC4IXCTahK1An6LkQMfsXb7Qus6ey241Vb4wTgFHqsdCx7qPxeAghmsTOHRVl\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/valid/024.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\n\ncfg = Config()\ncfg.set('limit_request_line', 0)\ncfg.set('limit_request_field_size', 0)\nrequest = {\n    \"method\": \"PUT\",\n    \"uri\":\n    uri(\"/q=08aP8931Ltyl9nqyJvjMaRCOgDV3uONtAdHABjoZUG6KAP6h3Vh97O3GJjjovXYgNdrhxc7TriXoAmeehZMJx88EyhcPXO0f09Nvd128SZnxZ2r5jFDELkn26reKRysODSLBZLfjU3vxLzLXKWeFOFJKcZYRH9V7hC98DDS4ZsS7weUksBuK6m86aLNHHHB0Xbyxv1TiDbOWYIzKxV0eZKyk0CaDLDiR0CRuMOf4rwBeuHoMrumzafrFI5iL72ANQZmOvKdk1qQeXkRqEG11YU0kF7f1hSlmgiIgg5maWiBsA9sAg36IIXZMWwJF63zpMgAyjTT8l4pQhSBfhY2xbGAWmLGpyd1rlBm0O5LCoKpnQuTACm2azi0x6a1Qbry9flQBO4jHge2dXiD1si6Gh5q8fZu8ZQ7LLWii2u4rGB7E4XlhnClrCHg5vJmjYf2AItYPA0ogsiIdEEQGpzMJPqrp8Icn5kAAimWF1aCYaDjcdSgWI48PnoxlzIHX50EPFcPOSLecjkstD9z66H554sUXfWn3Mk9lnOUlse6nx0u1YClFK4UFXp98ru9eBBr7pkAsfZ34yPskayGyXPPyzWyBfVd28UuvdEG47SMdyqEpX0rFdk67fAYij0PWMK79mDmGAS37O821o18XUbu0GQjsqAGVMN9LDIAliD9QqtlwdEnplKkUyyZ7GAFJCFffgzppU9CjA2FbPX6ZjTOi4sPoYEyhyeQKVqAe9keYeDpU2qDwq83XEDQUKvP0w48GyavSmdBcrMXjUsu0PfdYpSaKwarrUB3i93HgoQB3ZJIR4lW6iPRTmm28OEKq2MIJGAoTXxCZYM5UacRldlqQOj6JkYz6y7ppWOjJ9yiCUEenuvfcItgmw9HIgGA59JxO8NDLEZLSONfuIgiV7wjsJnxuTOlU4vkjV7fTuOeU91xez7UKhaTqqEW3XBUSLjhKi3IkZg7ukrGZTWPhijFv2EZwEWDAyLlHvZB4X738zGJUlEX1k52EHwrKVKdLfePcaOjAGKsongHBFYxYC8vBBLuKm9RWexKCT14M25pCGloJXZ4OpBRfDQA2kobLUcEXEpzqRBPGN2JdNSBOFlUtUxWKnnPBM6r9S356l3k1o9zTIPeoIitWRjASs4A0iwYc8p5vv5Kt8KtsmW7Xv8dlU8HbZHsy3LI7O9BpUH8cJubqdEhooKABkx71pdcsZGhZb6epyTiPyvOhdJ7tNtFy3KQOameqTgGyd53Z42eZ0AjaOEvnzermi2E0xo3MMHFhB74TFtNAI3ppxxyqknc1mzUqZ49Wi8YPBg9ids6IgZvddBQYvwEozkmyGAkatQtt9TD4LjU3TyyUlhNG21q7CzEEl8NNsVrV6QyHsfw7E5w7XcoT7OQkBYoZwHIAjfekehnpc2llRtRY5m43fPVasmsVazOR36DRSLZJPHAqUDO0LInu9mgP57Mnz9CgylEmdE2aaYs426rnTFR3G3CfjLofHfjaLOkAegr4W3jx6MNMMOMZw2u46YTCnlfbBK6ZA1UYeAH1DIQJykcSQESinC8HpYIJt9A8g7UT0awzRP1F9nHa3wDnaAHndQYKMrjzlWo8ejQ0XHWgHhqnWHgW4h9sOnJckH00CYK1fHUKASJ3D8kOKax6uplexfz6BCvAoL9zm5TjeB1yxrpLp9NjjTWSKG2HOZhPkGpdEqU4mjnN2AkUVACPGos5YLBmTnSrdOEGZJDlAvJOUt800Mu3BYc1MiDIB6LMSSV5RsIUDFOzNletGQoq4G3yHZmx78uEse5vUTPFF3KT8LCrssqdIU9H97Npgf6N5j8arQ7ykLzN459jJaUzpGIo6uowPnUSatDf9GAvAmWNvsVTz6bYiAV71C7QF0C7UolYIQY6DHJEHejgX2YMEovWNLPL50eeC51h4DdPNv5G4ZdNtQTRVybYBZMpetGDiFmXN0JKa1sKHOSZxdrhKjxDIhrYVyCcRUMQ0sjGGHFuOcRszr6E5igEMtsebHQ3KYiGd5B27LikpUHhk61rgZlulHdMoS6YgQs6SV6UMVNku6sCw529xhUciDwRMhsbAjDlahYbrGa3NryxyV5LrXONGGKCchCqv7vDMdAtPrVr8M2vL5MySQAC3g90iugGQcLH3hCf9f1Kn5X0hM4KZTfwOPJhlfJsMRNhssiDoXaycUvOUS58266yPDlitPIAzO03XClm4EDPXGIwcwiFr7FcDo3tQIMZVy87i48Zb80s3zAYRiBIS0vO3RKGx3OGN5zid2B7MfnfLzvpvgZoirHhAqXffnym5abpZNzGuo5GowTRA2Ptk4Ve2JFoHACWpD6HiGnRZ9QVOmPICoQrSUQw45Jlk9onKJz5Erhnx0943Uno6tMJ5jbrWBNiIO7i04xzRBgujeiAJvuQkVDX2QLKRxZ7s6rhdfOaq6R6uL108gEzzlXOLqTTJXgM63rcUWNbE7wsIXcCFSF59LLJ7G5Qea33suxdDX6DcK4a0VMZoxmWPtCi1dAT9ggJqc2Sh7mkAqizaB16RXZvSydchpdVj6s4qn4ivr0HKHdAstX0XZ0FFU6lOiNmU3vasMg2uaVG8tyuG8N8VsuXIOQs7xtFxDhilYb8MQ9vES9pWfWPSXFlJAq4XKPY8a0JOIx57EQuWHo3uWgRTIRThvZP9YYzSnjGIHwjS8JeppICHofADXZhJ0uDQaQs7MiXEALpGmT3W6w0G3tBdZcuTDkWx1HsT5jd9jQeJpgD2VxdKh8U4Q3vANTAuwBXLJ2P0stS8Q72JWgNPwKYTY9cPoaGZlUFGgVsq8CdEFH9yW0c27G5s5sfHsyep6t4VxIHHMOX2GmMRyGxDI33am1J7ZmJ1NyXiwkHxtPH5QBpU2PMu2Guf3xIxlk3snMkMAsGO0vYfqO9tdIgdxMYO3HZTYv99OXaHcNQ5u0pRZZyVrNOIPurkEOdJy0nowPemIgUuHWh8vQCuDZav1m35AOl6ftSFuChSm5KstEWnC7q8mJ0juJEBkCRmQphP3V1pqiDjz6YA90qEe7MA3nzT0nHG8A1hWlqcPVPNz4qWNF6Fq1ub4075aXO0H7Krb6rhWGb3ZRPjpb4BKN8jGFQrBUMZprtjAJ67BnfmYgE0mmGLV2QP10gYS1T06kBRyrtp7he6wsPiBPJ7wxPLHNUN2SGQHBTSKagndM99fuaga5Sw9OT8Fzdo7xUJXfhJ97gUnNDrknal0B00NMNvajZeQQTJyBsVSwBZtZ45ZCcq1idc7GWC0MITSk58cIVkSPXbrERUaygyY13dPeEVzjVi9aVJwUF6eJu1s8u3FCJqp2GoWIItwvZO69asX75fekFkmFpNavxM0X0dZC01TTPpV6E6PJoIfW8C06CKNHV7Gk2mkTWGSwUG4xD2L3G3XarodHDcmumFJX9Xviv0rvm38SCtin6OpjH8MHYDrj1OxTJbC2VclJxv73z2BDBquosKOik0fmgbPZN0FUTmjBEwHTvqd5QHTwb3nOpEz3X6YCF0lrcrQc0uhyr7gBGBs86nUBWFRp1LKjIRVTVXDipajqNDTQGNZtzvR9MUf1yJJV07inbrlPOENd7rHpKCrJtoZXOkDqInaIqoMCG3DVd353BGmZNJEKOa3DnL7fb9zwuHlvHAfCco7ZS4wAV87trWkp6skXux9v5WhkumbUyGq4ia6DM1PuqqnFfBTAWDzJsnggAJrzr8O7JbDtaXwcW9sqaOb0S6NvnUDZqiNdDQPMDOKvXRJJJQdf1FSrPCCSPEEWO1SeVwictj7rTbpWGRoukwhgJALys95pGGOQxCPzRGrtVFnGcsLN1CwI3wLbmDnNKUv3KpOLEOPRxQXeXuJRIiYCFum44c0wNr731DvHn3YEJMH4iwFONl1rolEL4w6KFUOCq7ekrE5iyUt1V32PNtuUshXRjOYjBval29JMH5GoqZlGhCczzHMA61cmuzqdFwiPCB9yzqvJTg8TqMNvwKJztFIQK4mc5Ev5rRVSozD796AVRKT8rZF39IA1kmCLdXqz7CCC8x4QjjDpxjKCXP5HkWf9mp2FNBjE3pAeaEc6Vk2ENLlW8WVCe08aP8931Ltyl9nqyJvjMaRCOgDV3uONtAdHABjoZUG6KAP6h3Vh97O3GJjjovXYgNdrhxc7TriXoAmeehZMJx88EyhcPXO0f09Nvd128SZnxZ2r5jFDELkn26reKRysODSLBZLfjU3vxLzLXKWeFOFJKcZYRH9V7hC98DDS4ZsS7weUksBuK6m86aLNHHHB0Xbyxv1TiDbOWYIzKxV0eZKyk0CaDLDiR0CRuMOf4rwBeuHoMrumzafrFI5iL72ANQZmOvKdk1qQeXkRqEG11YU0kF7f1hSlmgiIgg5maWiBsA9sAg36IIXZMWwJF63zpMgAyjTT8l4pQhSBfhY2xbGAWmLGpyd1rlBm0O5LCoKpnQuTACm2azi0x6a1Qbry9flQBO4jHge2dXiD1si6Gh5q8fZu8ZQ7LLWii2u4rGB7E4XlhnClrCHg5vJmjYf2AItYPA0ogsiIdEEQGpzMJPqrp8Icn5kAAimWF1aCYaDjcdSgWI48PnoxlzIHX50EPFcPOSLecjkstD9z66H554sUXfWn3Mk9lnOUlse6nx0u1YClFK4UFXp98ru9eBBr7pkAsfZ34yPskayGyXPPyzWyBfVd28UuvdEG47SMdyqEpX0rFdk67fAYij0PWMK79mDmGAS37O821o18XUbu0GQjsqAGVMN9LDIAliD9QqtlwdEnplKkUyyZ7GAFJCFffgzppU9CjA2FbPX6ZjTOi4sPoYEyhyeQKVqAe9keYeDpU2qDwq83XEDQUKvP0w48GyavSmdBcrMXjUsu0PfdYpSaKwarrUB3i93HgoQB3ZJIR4lW6iPRTmm28OEKq2MIJGAoTXxCZYM5UacRldlqQOj6JkYz6y7ppWOjJ9yiCUEenuvfcItgmw9HIgGA59JxO8NDLEZLSONfuIgiV7wjsJnxuTOlU4vkjV7fTuOeU91xez7UKhaTqqEW3XBUSLjhKi3IkZg7ukrGZTWPhijFv2EZwEWDAyLlHvZB4X738zGJUlEX1k52EHwrKVKdLfePcaOjAGKsongHBFYxYC8vBBLuKm9RWexKCT14M25pCGloJXZ4OpBRfDQA2kobLUcEXEpzqRBPGN2JdNSBOFlUtUxWKnnPBM6r9S356l3k1o9zTIPeoIitWRjASs4A0iwYc8p5vv5Kt8KtsmW7Xv8dlU8HbZHsy3LI7O9BpUH8cJubqdEhooKABkx71pdcsZGhZb6epyTiPyvOhdJ7tNtFy3KQOameqTgGyd53Z42eZ0AjaOEvnzermi2E0xo3MMHFhB74TFtNAI3ppxxyqknc1mzUqZ49Wi8YPBg9ids6IgZvddBQYvwEozkmyGAkatQtt9TD4LjU3TyyUlhNG21q7CzEEl8NNsVrV6QyHsfw7E5w7XcoT7OQkBYoZwHIAjfekehnpc2llRtRY5m43fPVasmsVazOR36DRSLZJPHAqUDO0LInu9mgP57Mnz9CgylEmdE2aaYs426rnTFR3G3CfjLofHfjaLOkAegr4W3jx6MNMMOMZw2u46YTCnlfbBK6ZA1UYeAH1DIQJykcSQESinC8HpYIJt9A8g7UT0awzRP1F9nHa3wDnaAHndQYKMrjzlWo8ejQ0XHWgHhqnWHgW4h9sOnJckH00CYK1fHUKASJ3D8kOKax6uplexfz6BCvAoL9zm5TjeB1yxrpLp9NjjTWSKG2HOZhPkGpdEqU4mjnN2AkUVACPGos5YLBmTnSrdOEGZJDlAvJOUt800Mu3BYc1MiDIB6LMSSV5RsIUDFOzNletGQoq4G3yHZmx78uEse5vUTPFF3KT8LCrssqdIU9H97Npgf6N5j8arQ7ykLzN459jJaUzpGIo6uowPnUSatDf9GAvAmWNvsVTz6bYiAV71C7QF0C7UolYIQY6DHJEHejgX2YMEovWNLPL50eeC51h4DdPNv5G4ZdNtQTRVybYBZMpetGDiFmXN0JKa1sKHOSZxdrhKjxDIhrYVyCcRUMQ0sjGGHFuOcRszr6E5igEMtsebHQ3KYiGd5B27LikpUHhk61rgZlulHdMoS6YgQs6SV6UMVNku6sCw529xhUciDwRMhsbAjDlahYbrGa3NryxyV5LrXONGGKCchCqv7vDMdAtPrVr8M2vL5MySQAC3g90iugGQcLH3hCf9f1Kn5X0hM4KZTfwOPJhlfJsMRNhssiDoXaycUvOUS58266yPDlitPIAzO03XClm4EDPXGIwcwiFr7FcDo3tQIMZVy87i48Zb80s3zAYRiBIS0vO3RKGx3OGN5zid2B7MfnfLzvpvgZoirHhAqXffnym5abpZNzGuo5GowTRA2Ptk4Ve2JFoHACWpD6HiGnRZ9QVOmPICoQrSUQw45Jlk9onKJz5Erhnx0943Uno6tMJ5jbrWBNiIO7i04xzRBgujeiAJvuQkVDX2QLKRxZ7s6rhdfOaq6R6uL108gEzzlXOLqTTJXgM63rcUWNbE7wsIXcCFSF59LLJ7G5Qea33suxdDX6DcK4a0VMZoxmWPtCi1dAT9ggJqc2Sh7mkAqizaB16RXZvSydchpdVj6s4qn4ivr0HKHdAstX0XZ0FFU6lOiNmU3vasMg2uaVG8tyuG8N8VsuXIOQs7xtFxDhilYb8MQ9vES9pWfWPSXFlJAq4XKPY8a0JOIx57EQuWHo3uWgRTIRThvZP9YYzSnjGIHwjS8JeppICHofADXZhJ0uDQaQs7MiXEALpGmT3W6w0G3tBdZcuTDkWx1HsT5jd9jQeJpgD2VxdKh8U4Q3vANTAuwBXLJ2P0stS8Q72JWgNPwKYTY9cPoaGZlUFGgVsq8CdEFH9yW0c27G5s5sfHsyep6t4VxIHHMOX2GmMRyGxDI33am1J7ZmJ1NyXiwkHxtPH5QBpU2PMu2Guf3xIxlk3snMkMAsGO0vYfqO9tdIgdxMYO3HZTYv99OXaHcNQ5u0pRZZyVrNOIPurkEOdJy0nowPemIgUuHWh8vQCuDZav1m35AOl6ftSFuChSm5KstEWnC7q8mJ0juJEBkCRmQphP3V1pqiDjz6YA90qEe7MA3nzT0nHG8A1hWlqcPVPNz4qWNF6Fq1ub4075aXO0H7Krb6rhWGb3ZRPjpb4BKN8jGFQrBUMZprtjAJ67BnfmYgE0mmGLV2QP10gYS1T06kBRyrtp7he6wsPiBPJ7wxPLHNUN2SGQHBTSKagndM99fuaga5Sw9OT8Fzdo7xUJXfhJ97gUnNDrknal0B00NMNvajZeQQTJyBsVSwBZtZ45ZCcq1idc7GWC0MITSk58cIVkSPXbrERUaygyY13dPeEVzjVi9aVJwUF6eJu1s8u3FCJqp2GoWIItwvZO69asX75fekFkmFpNavxM0X0dZC01TTPpV6E6PJoIfW8C06CKNHV7Gk2mkTWGSwUG4xD2L3G3XarodHDcmumFJX9Xviv0rvm38SCtin6OpjH8MHYDrj1OxTJbC2VclJxv73z2BDBquosKOik0fmgbPZN0FUTmjBEwHTvqd5QHTwb3nOpEz3X6YCF0lrcrQc0uhyr7gBGBs86nUBWFRp1LKjIRVTVXDipajqNDTQGNZtzvR9MUf1yJJV07inbrlPOENd7rHpKCrJtoZXOkDqInaIqoMCG3DVd353BGmZNJEKOa3DnL7fb9zwuHlvHAfCco7ZS4wAV87trWkp6skXux9v5WhkumbUyGq4ia6DM1PuqqnFfBTAWDzJsnggAJrzr8O7JbDtaXwcW9sqaOb0S6NvnUDZqiNdDQPMDOKvXRJJJQdf1FSrPCCSPEEWO1SeVwictj7rTbpWGRoukwhgJALys95pGGOQxCPzRGrtVFnGcsLN1CwI3wLbmDnNKUv3KpOLEOPRxQXeXuJRIiYCFum44c0wNr731DvHn3YEJMH4iwFONl1rolEL4w6KFUOCq7ekrE5iyUt1V32PNtuUshXRjOYjBval29JMH5GoqZlGhCczzHMA61cmuzqdFwiPCB9yzqvJTg8TqMNvwKJztFIQK4mc5Ev5rRVSozD796AVRKT8rZF39IA1kmCLdXqz7CCC8x4QjjDpxjKCXP5HkWf9mp2FNjE62a\"),\n    \"version\": (1, 0),\n    \"headers\": [\n        (\"SOMEHEADER\", \"0X0VfvRJPKiUBYDUS0Vbdm9Rv6pQ1giLdvXeG1SbOwwEjzKceTxd5RKlt9KHVdQkZPqnZ3jLsuj67otzLqX0Q1dY1EsBI1InsyGc2Dxdr5o7W5DsBGYV0SDMyta3V9bmBJXJQ6g8R9qPtNrED4eIPvVmFY7aokhFb4TILl5UnL8qI6qqiyniYDaPVMxDlZaoCNkDbukO34fOUJD6ZN541qmjWEq1rvtAYDI77mkzWSx5zOkYd62RFmY7YKrQC5gtIVq8SBLp09Ao53S3895ABRcxjrg99lfbgLQFYwbM4FQ6ab1Ll2uybZyEU8MHPt5Czst0cRsoG819SBphxygWcCNwB93KGLi1K9eiCuAgx6Ove165KObLrvfA1rDI5hiv83Gql0UohgKtHeRmtqM0McnCO1VWAnFxpi1hxIAlBrR4w35EcaryGEKKcL34QyzD1zlF4mkQkr1EAOTgIMKoLipGUgykz7UFN1cCuWyo3CkdZvukBS3IGtEfxFuFCcnp70WTIjZxXxU4owMbWW1ER5Gsx0ilET0mzekZL0ngCikNP2BRQikRdlVBQ3eiLzDjq27UAm7ufQ9MJla8Yxd6Ea37un9DMltQwGmnmeG5pET54STq72qfY4HCerWHbCX1qwHTErMfEfIWcYldDfytUTOj7NcWRga3xW7JYpPZHdlkb24evup3lI4arY6j5a12ZcX9zVI02IJG0QD9T4zSHEV0pdVFZ8xwOlSWKuZ9VZMmRyOwmfhIPA7fDV5SP8weRlSnSCSN4YBAfzFVNfPTyeoSfVpXsxIABhXEQTg12YvAAn9390wFhEhMsT9FWIiIs7oH63tQyjdEAZSJcZ0nSQfapvi4BDsQSMv3W2DofSzxwOPrVQWRMyvP0UV0J660Gc4iZ2Tixe3DSeqg9VuNvij09aCbkBdwJh9r4UWmM1Hp1ZDF5Rr14nKtFAgjVlGlfZi4bWQKTzOlqaVbWBvxdKsJ27eelyDnasIPqo17yY5lg10Lb8nyu60Wn7l7Xb0Ndp334B5am4Vh1foctvkkhNFeIejtnjPYmWjS77rJ1aL0zJka4Xog5Oparvc93Pddf9CzCxgle00BTKNj0syVo5uqvX5PVzdhAnigU4jdPbJbcPpbpJRU4UDqIswRNJOlGfpdLnCvnPIRB2a7btjFTaE0tne0TjedGbePje1Li21rPXPX7t5LICWl1SRyqQ9x9woGEv1sI5VgpRoKtS6oxWgMERjP3LcEez3XqLiSwv0rWMlDiJhxEopz8Mklx8ZygQLiwIYx2pNq0JhKB8K1lZ8dYE5d3nRWhXwG4gFTUg2JYjnjL81WGRmjXnZEVLwYfYBUkRlqWAYHi1E6wF85BfcwvkgnEeBTiQSlfu6xwCYaW2OEogq7tbdinvlpeEPij1qQivpcs573HPHpkXrEeXC9P2gZhmV1Rvn69NAN2lOXSVe8XotSyCG5fHFsTDYlOvYW8EBrAdWuZrwU753xwjk3QCp2ODetYze98voig4lfYHrrWT43VXcHt8J5z7U3kt5O460buwESBhgkALZdrFYyy4YQcmnAeSCw5OoLArDEmzaI4JkFBCDqQxTE9BTYA112r9ymuOo5MGkTDYZlvtvopG4ekorfLoIa13Z9L6ZilXT1cg55dvNlOrbTSHpQTYRJfJ6x71IpDFyvdbZbOHQYMm98fcN9CLqFErkpcN4JO26GIhSodGGTSnzyUxBYueawFNlGxCMTa6JseX9c7Xlo8NRaZHBPvG7Z4gUCkOdUSEW0RRTs3TSSdjEKnJ6u9RdDqqyvN8cJ7gliTd04mSyVnkmxdqVU8DrdIrkSCfVQNoFgdydDHS3wMLU6QGTGBzK5pd9EfsDEeYXtIb3CkRupM4SERGMTN8TyIxqqIyWmgjBmSGLTFOB5tsPhkVydVQNf7jBkDy6THfBy0uALVUkm2jLeTFXjajyeL4ms5Lgx0eLoz0XWN6WulXSA20zV3ObSCHbBeVUgKmPxHq5qPmAi04VFIvCOJ0rBQJh9ZHJMwvhI3VEBF6EmXOiRCn0XOhm3pfHlmaCAWrOSGuQs3NCNlFRjwmVRPY5FJrKYjH3FrLrLdU07zdViAix8C4LxVrRrMB6ligZC3CoDhFA4vMjiPU5SBRqRW4lwVnvMZEZbf0AYbBc2ymnKAOWbQwt2ldiI2qL0aLoL6YtSFUhpwMOR3LP1feUq6XRO5xc9V02nEt9MRQsl5MgmKMcXap4HqAN0yATpjAGRnWqEnE7E1XZg95cEl2gO4HXejKzR0kiTUudcw6P4t1RYLRx7isZNJxiq1JZz6FpEe7QhwGbhPySNMbXJtmYuhAaTpfGdGKMxvHHB9LmELOChdyfjHMwMZ2B0xgU2eJgJimCwLH3UEmExgAwJDD4GSCqevYAMK4P9FKPl0dku0KZ7uOJ8oNloEsrbvMuhuKFDuO1PNvxtdCcgASzNVzdueOtUm1giZIDqbb6j11nqi9NoFeck1zZi2kfGF7OeUp4vYszuhQNi4vd03QeVAduM9h9v36Nz1YobRxB2CjTp6qdKdW9IYBp8aExZpipnJIbfD2hTWE44kIu7Q17f4C9kycGjsLwAWkVbfTRmBMU8SbVKV1EJTrN1gGqGX7quSwg1Vp4qslKAk6EIkoReIl5DuzuH8Rbvrkp5LFFAhNhb1hvXvVWcibtDjQSradNtuYzGf2AAduhxOTnZjzbsceGYhQA5a5NtqxE2GBlW8CPoPzIyfMfPjdAIUmAcns7Fkp44nju2htwhryUyidEzDVyTwevquARjt5a7eu8qIKfPrYgbOAlPgA1JHNi55ivTNpDuQ8drNiafZIntA43HI447WtITYYvLxFRG8OWvJRwI0N7dvHYO8H8lYI1OwatfvLKlJqjtdJBBvMWXdT4SbxHUdNTDUQmqFGZaLx1AvYPnJTYRzrqn5ZnXyWQ1ZCwtvZK209TxoezJ2sGorE46C7Zyki6EcXlX2A8upUUh9IhqLYTzidIRrAPE5mZmosyDyShjnRiN5CLXZAI21eV4v3a6WXI8TKkUk3fhhajOgPXshlyCEfDAyESpz1J8RECu6vQs81E1ZNE5ha5UGw2wk3Ea8oSTfqTiu0OeisV2a6bfldvW4x0OL8PS57uuY0v0OZPSUPWmPQgnmJRVw8vmh62bpFekMnUH7y31fXU6MIyZaiBs1FEu7qF6irBszHt2ARy50SjgGwQZWcecgvB8gB874g3ES9mZer3diYGF3Wssmsm6XRdsNcuNn3yzuoi52cRrBYUOISegTBVApn4zfuCC9Y4AAfe6wmmiuN8hL6KJeOjrdK5EFQHGyrzeuIMaT3B2nKz1PNONVQ0udbqCQebz3cq7NPe6kGKFLiE6euWjdoMuAbuu8rTkAa42ensXz4a1Yo450ZVgYypaDtepDQWFkJyTHDW1HTVZfCok0tp7STRiQ8n3NKxOUSL9veuTsDs1FaV2rbzR3DvkEJrhJ10Rm0pvLgui5GUDKyWLnrqcNVtOIzFaj9K5pwMfnREm1VIs84ePX0GsMjirfOfubzDoYjavbiCtTB86nKx0tfCKtl0yUQ5PWSBqdGASY3mr5hZcFZ9bA6uXXGTNqMpUH3gqxCoF6t2yAim93t77jYkiFt3OBlBRVQzRsPbgEKRXbX3bWQj6NpDzNCQPYTs45HsQB967f4yByzLH8X289YAZJhJJyFTMCLbpdKFuMBX5Msyr4d15sBa1h5bI13dqU14WBnMKD12LkHMjHiyde6xf5EELf082sUfiAZaROFuDCDnA89p6y6oYEUgF1L9yQElZO4R6IrkJsEFN9hvARf3CH4ENqbYxtUN9gsB9CLCGKMy2R4wGKU3Dkyea27YCR4QHCdqX3HqOpy12uxBANvbrfEro9q5NJrGK7WVq3nNabN05x4TmIZk3asc8ehvDyhSgQLY0wwyvrkcYqNiETybJ57RjwVg1YE0IZEBfyAUNXE4goc2jtbZbHfcpTzt08pSJQZTAzuxrdQLS4EnaFHPpMdPh1YXUdclj6g2sjYbhoTYcV97bVDAUztMZ4EarUcv6tgQOvK66RmJCF2zVEpFDBS6AVZJWzrVlnuiweXpH0L9eY2Wy2EuAHi7gL4o0i0AkOapqY1TPUWUwBaVrKQzkL8QQbczgc97pMvSnGYMlcSdzlamFtUmRoOPmhBGMpVqmcxnstnqJ0TXMV65zbRN2hk3YVF5HwPjuWJmfkVYnyazuqKuaaohrQIe7YOOSAmD7C2vDnI50y1oScQqIPb87QAmguFz7jfNBSPymjPJ7UrToaJen7LEQr8S2b69ayZYNIyWbcpaW5ACUqdyT5AeHYhdENORnWS2B17qnBPtyvb4WujJCafLmsMFhQbcGonDZkHEOAnOcwRwJ4KIPr4MlQLRKsdnurPDDEmpCtCnFg8vPObOPHoHgICb9j35pG1YNhAAGIGTZ4g3JTJzFvTcW7GDRxREPZffKOuQTJoMYYaaPwnE0SainEpCFAukJbDy1ss5cZt60nqTw1asLzwMKJu5PHpU9sB9YN7J2cPhIbfb4387zSmSvqbt3I8NFjDbuYEhe6nZ7gRT5Th0W0MoyzHlmy4MSXbaAfUJNsLQJmdhdVKDsqMz0aXKIVNsXtn88owrhw0yqxU0K3IfTothafhpQ8daRUnbjzULViWRvUz7dI1N3GgylRzaEXQPgbj0DQ7RujNTcJoSp7I1ELjFFSBZDm4Jx5eXq0aS2SKJPFX7XmFfkkR99wRiHx4ByVTL5umojRhY5j8vg3l3yfliJbeOTXckaYiezrucuHaiVFWR2kjk9PUm57bDpvtSFMic652iDufj4hqpy5MH5r2lg67T6Bbb3fcq49cVJ3hkN2GfRqVhoPxmHyvotu5koheVh7oHDaLaf4VvcQMd5MF8sicaX3GXfoLjlfFZwfJBpXNbbVemD7XghpIEwuFjA1USU8yJnTdvCJ2bFmPNWFeWsBVDyl7XUsbgB3K2zz806xODZT639dqiqhGXQNbgYtShikQhiHhZF4wf4IY588LE4EO2bdXBb2Wezm8Gl2J5GAfqnx5Z6NF7h1gGkM27hpnmKNylKZjqTNANj0CRU4awpdVrYGX7hT0u452Y5bXpVl7cLuK7j2k7VG93NXPsXADhQA8R9WDcpU0PLzFWFq1omoQ9ZRSlvh8R4pRp4vHIYf4A5uQEmv5Owr4pFQcWdp5GAdkpBaSHvUhvMxOSpsqVB2LHvvs1RiOUHHhHdZEKpX25mK9moud8pKT4efru1SlRRSsxdz87hTJMUrueydHDPXbo9AvExctdqxuCk03Fy8cB57qrkQQ50oGNuTNPColMrwVfmuTt81uSZremLbINILnCVXEnvTugRQfFYMnprqMB4mVJfZfh6XVLdOyW4BPaFrBsZGFy7udoWJwE8ACx4UpJW6m1ltckofzA6AUxzXprXDCCL118m8bBB2hzDKmqeLk5ZYKsLROkTqRAxmJjBSZSo2XBroO5rVvkOZrOZRe8NgaHFMLPn0I6hsqwA7VdKlpbqknax84iWrtBe8ErxgPIQeYhELyK1deW1YWBagD21MBTc2h5LliIlglZg41H8Zl3GvUv0XNZegR5bx1kiM9WFGV9Yt37iQQGquWAMKCAb6AqpkCtKs7sXKaEAVsbh32tlkAg4ngspjwzYHTPYKUuigPX5K8siUfaAW9WJl7r8dc4ju97osWETOcBENLsfwB66TvsttORtOedylnErplZP3hjt7o39JllXDobj3l10bSr4B09eYVWi2DLGavYktKSKj1PrqzuGUaqcFxqoebpuDEAx5vl8ZmSYrmS2RBJ1n2s3lkKdaVWTmfIXlyMMT7Ac3lCXpGNnpf8ccTffv3E0fBrpCSpVc48dM5e5iTpRPrfWxAjrud9jSrqVBXsw3pqUvhuVmBpmwoKAfQGxHrauna3f48AFefGDozxXXjpdM9ZDWHsRUBTFNzDs8tUATtegSzZfNJCS9k0p5q2cueyU1mtwMJIdf0FrsVGiAyX7PFkWvLHi29fpprZQd0gbMMw2Bt10ZbZCsjPX261cXmVa6ZPnkVQm2w1ory3uWejuq20oQCyXTYyv1Ki4tbdPxoNn04Je7uS3QHDCsUl4i9zKNhBJ3g55bhIZWfwmLi3S7oY16gImdC6vvjsMKkCPzXv4pPaVhHH7o4f0mWEz30k4o7GQNOUy8LPM3NmlZF7QaIBdRfozG86jwQkC3jTNR357pdPjOqMERtIS4WEJBgbaeUCu5MOhsNdaD91iCeghIpOECFyTdEkUCGPPCIAtuAOKBdhPu40UxHx30dELMTK3azHOuOnLTsdiM4KJ9yF4Ab2eiz5j2T95sDx3aiEJDVDPCa55hO0XTBM9OSNtdzjdTdZT19XrwD0wPWZcBhfJ66X1uNM2eud1btzglqZP52qqYU7BK2M3BBZKKjy7P6YzmgaPHWnFGHZdwdz3Yq6e3N76Cjkfl8Sy0mkwd6pt0geDM1jNNZrcT8dUfLLaiUqcZm1KRVdpZaBrboDSuCxfWYlxqgsldwlGL4C06ceFUDXX8PzxzWEgOd8OU4F22pcNJOnwJGo6rYA3tvhAuq2WKVg6tgFCb1p7dzF4Ke3J0dv3IneMSNnHG4hkvxW6VzIykDUtYEjMQO35tdnEA0vMVLXIahpJpz4HGs5wwRgoZx1e1zD1pXi7KmEVTlfattgcGFlKjZJ60fEdloZEmiXodxT63CzuJHnjHDOL8qcMzTxHb8OCainga4w1fk4uILLAWqmTFpDcFGSF5lbOFUwhvtMK6knIWZ8ZApZvTGBt1qv3xKUJqPcWiweI4kk57zgyTPZku2mg4fJWDKSfiRSi7LvtpKkdqjein9lP7LMv5lKutprVzjmvHBPjunXGqakWx39xYH8RD6qF3Fw2BnIIesiicZsDv69Ggbu9Y334UeFPNIJ3LGp2I8xcUxlP5dJAh4V05p1HvIZ5Fhk0oCWlvNXdLqzbVsbfW9jWyQTaZXzw7WT3rqFQc7wvw4ayp5eKmUclqB1yOvrI14XGhmH7QMaAYNTIE2RHjYXVgvbmFRi0oB1v4nDEeSTn3KHBRQD8TilCagKg0XYPj2eAgWs12ZRYzlGyCvYZ1pol5wAwc9AFFGwsTJ9UYkbxlZv7wKDx7nFzlUSMC1kMvS2ECwvHzSycqHPRwCGipvG6kWz0mGvASXeKjm47iMROoY0MRK0uvgNdTTOTdxkMgOuCDIlxfit5QKjyzaVAg2kDwENfSd6XPMgSprTSLuNDXdg5NHCwUvDbEHVxpMgOItZymPZtPweOrnPdlEB4UwLZ8jqtShi5oDYvhkh85FwwT25OHFvDUWTTCV5n73pQ8kLo8zsB3mbWfGwg62guj3C50Dh42fAZEPBRSHDRTg3r0z39Vyj490lk2UpZeNyylwuEKmuIqEkbE3BRT2YEjTM8a2PU5grCuzculibcoRUpb1sIQiMRTf4wrtT1CnKcoUJ1T28DC04dTJVRcm3w3WzNLdrnovkX6NahblTzDvq5eXkoEaZv6HClmGuho4FH6s6i0OdmmW8qkNOnk7BhexiyAd3UYERlFwvZ6LP55tFOc3vnlhyylx1rTTgu1NFljRNs7rGiT7SnGFaFK7GITEZFEYI7DmOEUZXxDSHjYuOVN0YAJP2cZFgagyMwGJdrpH8S7cewYPMKz2Go2GBKl1OA6pJ8T91tUdEcGVg9JCMQUA4sBtlIuRTVV3cduIhsLCTi2ewItkh9MRP1kevVa9WcXejQQKreZmq5EZtzThW71r7E2tcvwFeqiwv3JZnV16bZ7NwZT6uvSrOnIFUyMsxhh8xCkVY82VLTAZhPXB8t6CbyjZ5stos6WmNZgoEsD8GU8pmzSTubAqQXkTbiODF2pePe6S9uQ9HngGGBnOjY4QUcAcScDsfflyXVqyxgTelGD4vXoba6qRWCqc9LKpyk4jCKYvLX9tzXusO7bhT2KRvF4MObDqdE4KnCCIF3zeVD0vImR20MmRTBHRCNm3s6GfyeTYEAlW3L2igZJ7Myj5zGLccMt2EohGc38HfWZ4mlvXRLHKB233PyKALYifqlAxTXaWUk13o6nACQDvN7DxSCA0daJeuznK1Dr52bC4IXCTahK1An6LkQMfsXb7Qus6ey241Vb4wTgFHqsdCx7qPxeAghmsTOHRVl\")\n    ],\n    \"body\": ''\n}\n"
  },
  {
    "path": "tests/requests/valid/025.http",
    "content": "POST /chunked HTTP/1.1\\r\\n\nTransfer-Encoding: gzip\\r\\n\nTransfer-Encoding: chunked\\r\\n\n\\r\\n\n5\\r\\n\nhello\\r\\n\n6\\r\\n\n world\\r\\n\n0\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/valid/025.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nrequest = {\n    \"method\": \"POST\",\n    \"uri\": uri(\"/chunked\"),\n    \"version\": (1, 1),\n    \"headers\": [\n        ('TRANSFER-ENCODING', 'gzip'),\n        ('TRANSFER-ENCODING', 'chunked')\n    ],\n    \"body\": b\"hello world\"\n}\n"
  },
  {
    "path": "tests/requests/valid/025_line.http",
    "content": "POST /chunked HTTP/1.1\\r\\n\nTransfer-Encoding: gzip,chunked\\r\\n\n\\r\\n\n5\\r\\n\nhello\\r\\n\n6\\r\\n\n world\\r\\n\n0\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/valid/025_line.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nrequest = {\n    \"method\": \"POST\",\n    \"uri\": uri(\"/chunked\"),\n    \"version\": (1, 1),\n    \"headers\": [\n        ('TRANSFER-ENCODING', 'gzip,chunked')\n\n    ],\n    \"body\": b\"hello world\"\n}\n"
  },
  {
    "path": "tests/requests/valid/026.http",
    "content": "GET / HTTP/1.0\\r\\n\nSomeheader: 0X0VfvRJPKiUBYDUS0Vbdm9Rv6pQ1giLdvXeG1SbOwwEjzKceTxd5RKlt9KHVdQkZPqnZ3jLsuj67otzLqX0Q1dY1EsBI1InsyGc2Dxdr5o7W5DsBGYV0SDMyta3V9bmBJXJQ6g8R9qPtNrED4eIPvVmFY7aokhFb4TILl5UnL8qI6qqiyniYDaPVMxDlZaoCNkDbukO34fOUJD6ZN541qmjWEq1rvtAYDI77mkzWSx5zOkYd62RFmY7YKrQC5gtIVq8SBLp09Ao53S3895ABRcxjrg99lfbgLQFYwbM4FQ6ab1Ll2uybZyEU8MHPt5Czst0cRsoG819SBphxygWcCNwB93KGLi1K9eiCuAgx6Ove165KObLrvfA1rDI5hiv83Gql0UohgKtHeRmtqM0McnCO1VWAnFxpi1hxIAlBrR4w35EcaryGEKKcL34QyzD1zlF4mkQkr1EAOTgIMKoLipGUgykz7UFN1cCuWyo3CkdZvukBS3IGtEfxFuFCcnp70WTIjZxXxU4owMbWW1ER5Gsx0ilET0mzekZL0ngCikNP2BRQikRdlVBQ3eiLzDjq27UAm7ufQ9MJla8Yxd6Ea37un9DMltQwGmnmeG5pET54STq72qfY4HCerWHbCX1qwHTErMfEfIWcYldDfytUTOj7NcWRga3xW7JYpPZHdlkb24evup3lI4arY6j5a12ZcX9zVI02IJG0QD9T4zSHEV0pdVFZ8xwOlSWKuZ9VZMmRyOwmfhIPA7fDV5SP8weRlSnSCSN4YBAfzFVNfPTyeoSfVpXsxIABhXEQTg12YvAAn9390wFhEhMsT9FWIiIs7oH63tQyjdEAZSJcZ0nSQfapvi4BDsQSMv3W2DofSzxwOPrVQWRMyvP0UV0J660Gc4iZ2Tixe3DSeqg9VuNvij09aCbkBdwJh9r4UWmM1Hp1ZDF5Rr14nKtFAgjVlGlfZi4bWQKTzOlqaVbWBvxdKsJ27eelyDnasIPqo17yY5lg10Lb8nyu60Wn7l7Xb0Ndp334B5am4Vh1foctvkkhNFeIejtnjPYmWjS77rJ1aL0zJka4Xog5Oparvc93Pddf9CzCxgle00BTKNj0syVo5uqvX5PVzdhAnigU4jdPbJbcPpbpJRU4UDqIswRNJOlGfpdLnCvnPIRB2a7btjFTaE0tne0TjedGbePje1Li21rPXPX7t5LICWl1SRyqQ9x9woGEv1sI5VgpRoKtS6oxWgMERjP3LcEez3XqLiSwv0rWMlDiJhxEopz8Mklx8ZygQLiwIYx2pNq0JhKB8K1lZ8dYE5d3nRWhXwG4gFTUg2JYjnjL81WGRmjXnZEVLwYfYBUkRlqWAYHi1E6wF85BfcwvkgnEeBTiQSlfu6xwCYaW2OEogq7tbdinvlpeEPij1qQivpcs573HPHpkXrEeXC9P2gZhmV1Rvn69NAN2lOXSVe8XotSyCG5fHFsTDYlOvYW8EBrAdWuZrwU753xwjk3QCp2ODetYze98voig4lfYHrrWT43VXcHt8J5z7U3kt5O460buwESBhgkALZdrFYyy4YQcmnAeSCw5OoLArDEmzaI4JkFBCDqQxTE9BTYA112r9ymuOo5MGkTDYZlvtvopG4ekorfLoIa13Z9L6ZilXT1cg55dvNlOrbTSHpQTYRJfJ6x71IpDFyvdbZbOHQYMm98fcN9CLqFErkpcN4JO26GIhSodGGTSnzyUxBYueawFNlGxCMTa6JseX9c7Xlo8NRaZHBPvG7Z4gUCkOdUSEW0RRTs3TSSdjEKnJ6u9RdDqqyvN8cJ7gliTd04mSyVnkmxdqVU8DrdIrkSCfVQNoFgdydDHS3wMLU6QGTGBzK5pd9EfsDEeYXtIb3CkRupM4SERGMTN8TyIxqqIyWmgjBmSGLTFOB5tsPhkVydVQNf7jBkDy6THfBy0uALVUkm2jLeTFXjajyeL4ms5Lgx0eLoz0XWN6WulXSA20zV3ObSCHbBeVUgKmPxHq5qPmAi04VFIvCOJ0rBQJh9ZHJMwvhI3VEBF6EmXOiRCn0XOhm3pfHlmaCAWrOSGuQs3NCNlFRjwmVRPY5FJrKYjH3FrLrLdU07zdViAix8C4LxVrRrMB6ligZC3CoDhFA4vMjiPU5SBRqRW4lwVnvMZEZbf0AYbBc2ymnKAOWbQwt2ldiI2qL0aLoL6YtSFUhpwMOR3LP1feUq6XRO5xc9V02nEt9MRQsl5MgmKMcXap4HqAN0yATpjAGRnWqEnE7E1XZg95cEl2gO4HXejKzR0kiTUudcw6P4t1RYLRx7isZNJxiq1JZz6FpEe7QhwGbhPySNMbXJtmYuhAaTpfGdGKMxvHHB9LmELOChdyfjHMwMZ2B0xgU2eJgJimCwLH3UEmExgAwJDD4GSCqevYAMK4P9FKPl0dku0KZ7uOJ8oNloEsrbvMuhuKFDuO1PNvxtdCcgASzNVzdueOtUm1giZIDqbb6j11nqi9NoFeck1zZi2kfGF7OeUp4vYszuhQNi4vd03QeVAduM9h9v36Nz1YobRxB2CjTp6qdKdW9IYBp8aExZpipnJIbfD2hTWE44kIu7Q17f4C9kycGjsLwAWkVbfTRmBMU8SbVKV1EJTrN1gGqGX7quSwg1Vp4qslKAk6EIkoReIl5DuzuH8Rbvrkp5LFFAhNhb1hvXvVWcibtDjQSradNtuYzGf2AAduhxOTnZjzbsceGYhQA5a5NtqxE2GBlW8CPoPzIyfMfPjdAIUmAcns7Fkp44nju2htwhryUyidEzDVyTwevquARjt5a7eu8qIKfPrYgbOAlPgA1JHNi55ivTNpDuQ8drNiafZIntA43HI447WtITYYvLxFRG8OWvJRwI0N7dvHYO8H8lYI1OwatfvLKlJqjtdJBBvMWXdT4SbxHUdNTDUQmqFGZaLx1AvYPnJTYRzrqn5ZnXyWQ1ZCwtvZK209TxoezJ2sGorE46C7Zyki6EcXlX2A8upUUh9IhqLYTzidIRrAPE5mZmosyDyShjnRiN5CLXZAI21eV4v3a6WXI8TKkUk3fhhajOgPXshlyCEfDAyESpz1J8RECu6vQs81E1ZNE5ha5UGw2wk3Ea8oSTfqTiu0OeisV2a6bfldvW4x0OL8PS57uuY0v0OZPSUPWmPQgnmJRVw8vmh62bpFekMnUH7y31fXU6MIyZaiBs1FEu7qF6irBszHt2ARy50SjgGwQZWcecgvB8gB874g3ES9mZer3diYGF3Wssmsm6XRdsNcuNn3yzuoi52cRrBYUOISegTBVApn4zfuCC9Y4AAfe6wmmiuN8hL6KJeOjrdK5EFQHGyrzeuIMaT3B2nKz1PNONVQ0udbqCQebz3cq7NPe6kGKFLiE6euWjdoMuAbuu8rTkAa42ensXz4a1Yo450ZVgYypaDtepDQWFkJyTHDW1HTVZfCok0tp7STRiQ8n3NKxOUSL9veuTsDs1FaV2rbzR3DvkEJrhJ10Rm0pvLgui5GUDKyWLnrqcNVtOIzFaj9K5pwMfnREm1VIs84ePX0GsMjirfOfubzDoYjavbiCtTB86nKx0tfCKtl0yUQ5PWSBqdGASY3mr5hZcFZ9bA6uXXGTNqMpUH3gqxCoF6t2yAim93t77jYkiFt3OBlBRVQzRsPbgEKRXbX3bWQj6NpDzNCQPYTs45HsQB967f4yByzLH8X289YAZJhJJyFTMCLbpdKFuMBX5Msyr4d15sBa1h5bI13dqU14WBnMKD12LkHMjHiyde6xf5EELf082sUfiAZaROFuDCDnA89p6y6oYEUgF1L9yQElZO4R6IrkJsEFN9hvARf3CH4ENqbYxtUN9gsB9CLCGKMy2R4wGKU3Dkyea27YCR4QHCdqX3HqOpy12uxBANvbrfEro9q5NJrGK7WVq3nNabN05x4TmIZk3asc8ehvDyhSgQLY0wwyvrkcYqNiETybJ57RjwVg1YE0IZEBfyAUNXE4goc2jtbZbHfcpTzt08pSJQZTAzuxrdQLS4EnaFHPpMdPh1YXUdclj6g2sjYbhoTYcV97bVDAUztMZ4EarUcv6tgQOvK66RmJCF2zVEpFDBS6AVZJWzrVlnuiweXpH0L9eY2Wy2EuAHi7gL4o0i0AkOapqY1TPUWUwBaVrKQzkL8QQbczgc97pMvSnGYMlcSdzlamFtUmRoOPmhBGMpVqmcxnstnqJ0TXMV65zbRN2hk3YVF5HwPjuWJmfkVYnyazuqKuaaohrQIe7YOOSAmD7C2vDnI50y1oScQqIPb87QAmguFz7jfNBSPymjPJ7UrToaJen7LEQr8S2b69ayZYNIyWbcpaW5ACUqdyT5AeHYhdENORnWS2B17qnBPtyvb4WujJCafLmsMFhQbcGonDZkHEOAnOcwRwJ4KIPr4MlQLRKsdnurPDDEmpCtCnFg8vPObOPHoHgICb9j35pG1YNhAAGIGTZ4g3JTJzFvTcW7GDRxREPZffKOuQTJoMYYaaPwnE0SainEpCFAukJbDy1ss5cZt60nqTw1asLzwMKJu5PHpU9sB9YN7J2cPhIbfb4387zSmSvqbt3I8NFjDbuYEhe6nZ7gRT5Th0W0MoyzHlmy4MSXbaAfUJNsLQJmdhdVKDsqMz0aXKIVNsXtn88owrhw0yqxU0K3IfTothafhpQ8daRUnbjzULViWRvUz7dI1N3GgylRzaEXQPgbj0DQ7RujNTcJoSp7I1ELjFFSBZDm4Jx5eXq0aS2SKJPFX7XmFfkkR99wRiHx4ByVTL5umojRhY5j8vg3l3yfliJbeOTXckaYiezrucuHaiVFWR2kjk9PUm57bDpvtSFMic652iDufj4hqpy5MH5r2lg67T6Bbb3fcq49cVJ3hkN2GfRqVhoPxmHyvotu5koheVh7oHDaLaf4VvcQMd5MF8sicaX3GXfoLjlfFZwfJBpXNbbVemD7XghpIEwuFjA1USU8yJnTdvCJ2bFmPNWFeWsBVDyl7XUsbgB3K2zz806xODZT639dqiqhGXQNbgYtShikQhiHhZF4wf4IY588LE4EO2bdXBb2Wezm8Gl2J5GAfqnx5Z6NF7h1gGkM27hpnmKNylKZjqTNANj0CRU4awpdVrYGX7hT0u452Y5bXpVl7cLuK7j2k7VG93NXPsXADhQA8R9WDcpU0PLzFWFq1omoQ9ZRSlvh8R4pRp4vHIYf4A5uQEmv5Owr4pFQcWdp5GAdkpBaSHvUhvMxOSpsqVB2LHvvs1RiOUHHhHdZEKpX25mK9moud8pKT4efru1SlRRSsxdz87hTJMUrueydHDPXbo9AvExctdqxuCk03Fy8cB57qrkQQ50oGNuTNPColMrwVfmuTt81uSZremLbINILnCVXEnvTugRQfFYMnprqMB4mVJfZfh6XVLdOyW4BPaFrBsZGFy7udoWJwE8ACx4UpJW6m1ltckofzA6AUxzXprXDCCL118m8bBB2hzDKmqeLk5ZYKsLROkTqRAxmJjBSZSo2XBroO5rVvkOZrOZRe8NgaHFMLPn0I6hsqwA7VdKlpbqknax84iWrtBe8ErxgPIQeYhELyK1deW1YWBagD21MBTc2h5LliIlglZg41H8Zl3GvUv0XNZegR5bx1kiM9WFGV9Yt37iQQGquWAMKCAb6AqpkCtKs7sXKaEAVsbh32tlkAg4ngspjwzYHTPYKUuigPX5K8siUfaAW9WJl7r8dc4ju97osWETOcBENLsfwB66TvsttORtOedylnErplZP3hjt7o39JllXDobj3l10bSr4B09eYVWi2DLGavYktKSKj1PrqzuGUaqcFxqoebpuDEAx5vl8ZmSYrmS2RBJ1n2s3lkKdaVWTmfIXlyMMT7Ac3lCXpGNnpf8ccTffv3E0fBrpCSpVc48dM5e5iTpRPrfWxAjrud9jSrqVBXsw3pqUvhuVmBpmwoKAfQGxHrauna3f48AFefGDozxXXjpdM9ZDWHsRUBTFNzDs8tUATtegSzZfNJCS9k0p5q2cueyU1mtwMJIdf0FrsVGiAyX7PFkWvLHi29fpprZQd0gbMMw2Bt10ZbZCsjPX261cXmVa6ZPnkVQm2w1ory3uWejuq20oQCyXTYyv1Ki4tbdPxoNn04Je7uS3QHDCsUl4i9zKNhBJ3g55bhIZWfwmLi3S7oY16gImdC6vvjsMKkCPzXv4pPaVhHH7o4f0mWEz30k4o7GQNOUy8LPM3NmlZF7QaIBdRfozG86jwQkC3jTNR357pdPjOqMERtIS4WEJBgbaeUCu5MOhsNdaD91iCeghIpOECFyTdEkUCGPPCIAtuAOKBdhPu40UxHx30dELMTK3azHOuOnLTsdiM4KJ9yF4Ab2eiz5j2T95sDx3aiEJDVDPCa55hO0XTBM9OSNtdzjdTdZT19XrwD0wPWZcBhfJ66X1uNM2eud1btzglqZP52qqYU7BK2M3BBZKKjy7P6YzmgaPHWnFGHZdwdz3Yq6e3N76Cjkfl8Sy0mkwd6pt0geDM1jNNZrcT8dUfLLaiUqcZm1KRVdpZaBrboDSuCxfWYlxqgsldwlGL4C06ceFUDXX8PzxzWEgOd8OU4F22pcNJOnwJGo6rYA3tvhAuq2WKVg6tgFCb1p7dzF4Ke3J0dv3IneMSNnHG4hkvxW6VzIykDUtYEjMQO35tdnEA0vMVLXIahpJpz4HGs5wwRgoZx1e1zD1pXi7KmEVTlfattgcGFlKjZJ60fEdloZEmiXodxT63CzuJHnjHDOL8qcMzTxHb8OCainga4w1fk4uILLAWqmTFpDcFGSF5lbOFUwhvtMK6knIWZ8ZApZvTGBt1qv3xKUJqPcWiweI4kk57zgyTPZku2mg4fJWDKSfiRSi7LvtpKkdqjein9lP7LMv5lKutprVzjmvHBPjunXGqakWx39xYH8RD6qF3Fw2BnIIesiicZsDv69Ggbu9Y334UeFPNIJ3LGp2I8xcUxlP5dJAh4V05p1HvIZ5Fhk0oCWlvNXdLqzbVsbfW9jWyQTaZXzw7WT3rqFQc7wvw4ayp5eKmUclqB1yOvrI14XGhmH7QMaAYNTIE2RHjYXVgvbmFRi0oB1v4nDEeSTn3KHBRQD8TilCagKg0XYPj2eAgWs12ZRYzlGyCvYZ1pol5wAwc9AFFGwsTJ9UYkbxlZv7wKDx7nFzlUSMC1kMvS2ECwvHzSycqHPRwCGipvG6kWz0mGvASXeKjm47iMROoY0MRK0uvgNdTTOTdxkMgOuCDIlxfit5QKjyzaVAg2kDwENfSd6XPMgSprTSLuNDXdg5NHCwUvDbEHVxpMgOItZymPZtPweOrnPdlEB4UwLZ8jqtShi5oDYvhkh85FwwT25OHFvDUWTTCV5n73pQ8kLo8zsB3mbWfGwg62guj3C50Dh42fAZEPBRSHDRTg3r0z39Vyj490lk2UpZeNyylwuEKmuIqEkbE3BRT2YEjTM8a2PU5grCuzculibcoRUpb1sIQiMRTf4wrtT1CnKcoUJ1T28DC04dTJVRcm3w3WzNLdrnovkX6NahblTzDvq5eXkoEaZv6HClmGuho4FH6s6i0OdmmW8qkNOnk7BhexiyAd3UYERlFwvZ6LP55tFOc3vnlhyylx1rTTgu1NFljRNs7rGiT7SnGFaFK7GITEZFEYI7DmOEUZXxDSHjYuOVN0YAJP2cZFgagyMwGJdrpH8S7cewYPMKz2Go2GBKl1OA6pJ8T91tUdEcGVg9JCMQUA4sBtlIuRTVV3cduIhsLCTi2ewItkh9MRP1kevVa9WcXejQQKreZmq5EZtzThW71r7E2tcvwFeqiwv3JZnV16bZ7NwZT6uvSrOnIFUyMsxhh8xCkVY82VLTAZhPXB8t6CbyjZ5stos6WmNZgoEsD8GU8pmzSTubAqQXkTbiODF2pePe6S9uQ9HngGGBnOjY4QUcAcScDsfflyXVqyxgTelGD4vXoba6qRWCqc9LKpyk4jCKYvLX9tzXusO7bhT2KRvF4MObDqdE4KnCCIF3zeVD0vImR20MmRTBHRCNm3s6GfyeTYEAlW3L2igZJ7Myj5zGLccMt2EohGc38HfWZ4mlvXRLHKB233PyKALYifqlAxTXaWUk13o6nACQDvN7DxSCA0daJeuznK1Dr52bC4IXCTahK1An6LkQMfsXb7Qus6ey241Vb4wTgFHqsdCx7qPxeAghmsTOHRVl\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/valid/026.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\n\ncfg = Config()\ncfg.set('limit_request_line', 0)\ncfg.set('limit_request_field_size', 8210)\nrequest = {\n    \"method\": \"GET\",\n    \"uri\": uri(\"/\"),\n    \"version\": (1, 0),\n    \"headers\": [\n        (\"SOMEHEADER\", \"0X0VfvRJPKiUBYDUS0Vbdm9Rv6pQ1giLdvXeG1SbOwwEjzKceTxd5RKlt9KHVdQkZPqnZ3jLsuj67otzLqX0Q1dY1EsBI1InsyGc2Dxdr5o7W5DsBGYV0SDMyta3V9bmBJXJQ6g8R9qPtNrED4eIPvVmFY7aokhFb4TILl5UnL8qI6qqiyniYDaPVMxDlZaoCNkDbukO34fOUJD6ZN541qmjWEq1rvtAYDI77mkzWSx5zOkYd62RFmY7YKrQC5gtIVq8SBLp09Ao53S3895ABRcxjrg99lfbgLQFYwbM4FQ6ab1Ll2uybZyEU8MHPt5Czst0cRsoG819SBphxygWcCNwB93KGLi1K9eiCuAgx6Ove165KObLrvfA1rDI5hiv83Gql0UohgKtHeRmtqM0McnCO1VWAnFxpi1hxIAlBrR4w35EcaryGEKKcL34QyzD1zlF4mkQkr1EAOTgIMKoLipGUgykz7UFN1cCuWyo3CkdZvukBS3IGtEfxFuFCcnp70WTIjZxXxU4owMbWW1ER5Gsx0ilET0mzekZL0ngCikNP2BRQikRdlVBQ3eiLzDjq27UAm7ufQ9MJla8Yxd6Ea37un9DMltQwGmnmeG5pET54STq72qfY4HCerWHbCX1qwHTErMfEfIWcYldDfytUTOj7NcWRga3xW7JYpPZHdlkb24evup3lI4arY6j5a12ZcX9zVI02IJG0QD9T4zSHEV0pdVFZ8xwOlSWKuZ9VZMmRyOwmfhIPA7fDV5SP8weRlSnSCSN4YBAfzFVNfPTyeoSfVpXsxIABhXEQTg12YvAAn9390wFhEhMsT9FWIiIs7oH63tQyjdEAZSJcZ0nSQfapvi4BDsQSMv3W2DofSzxwOPrVQWRMyvP0UV0J660Gc4iZ2Tixe3DSeqg9VuNvij09aCbkBdwJh9r4UWmM1Hp1ZDF5Rr14nKtFAgjVlGlfZi4bWQKTzOlqaVbWBvxdKsJ27eelyDnasIPqo17yY5lg10Lb8nyu60Wn7l7Xb0Ndp334B5am4Vh1foctvkkhNFeIejtnjPYmWjS77rJ1aL0zJka4Xog5Oparvc93Pddf9CzCxgle00BTKNj0syVo5uqvX5PVzdhAnigU4jdPbJbcPpbpJRU4UDqIswRNJOlGfpdLnCvnPIRB2a7btjFTaE0tne0TjedGbePje1Li21rPXPX7t5LICWl1SRyqQ9x9woGEv1sI5VgpRoKtS6oxWgMERjP3LcEez3XqLiSwv0rWMlDiJhxEopz8Mklx8ZygQLiwIYx2pNq0JhKB8K1lZ8dYE5d3nRWhXwG4gFTUg2JYjnjL81WGRmjXnZEVLwYfYBUkRlqWAYHi1E6wF85BfcwvkgnEeBTiQSlfu6xwCYaW2OEogq7tbdinvlpeEPij1qQivpcs573HPHpkXrEeXC9P2gZhmV1Rvn69NAN2lOXSVe8XotSyCG5fHFsTDYlOvYW8EBrAdWuZrwU753xwjk3QCp2ODetYze98voig4lfYHrrWT43VXcHt8J5z7U3kt5O460buwESBhgkALZdrFYyy4YQcmnAeSCw5OoLArDEmzaI4JkFBCDqQxTE9BTYA112r9ymuOo5MGkTDYZlvtvopG4ekorfLoIa13Z9L6ZilXT1cg55dvNlOrbTSHpQTYRJfJ6x71IpDFyvdbZbOHQYMm98fcN9CLqFErkpcN4JO26GIhSodGGTSnzyUxBYueawFNlGxCMTa6JseX9c7Xlo8NRaZHBPvG7Z4gUCkOdUSEW0RRTs3TSSdjEKnJ6u9RdDqqyvN8cJ7gliTd04mSyVnkmxdqVU8DrdIrkSCfVQNoFgdydDHS3wMLU6QGTGBzK5pd9EfsDEeYXtIb3CkRupM4SERGMTN8TyIxqqIyWmgjBmSGLTFOB5tsPhkVydVQNf7jBkDy6THfBy0uALVUkm2jLeTFXjajyeL4ms5Lgx0eLoz0XWN6WulXSA20zV3ObSCHbBeVUgKmPxHq5qPmAi04VFIvCOJ0rBQJh9ZHJMwvhI3VEBF6EmXOiRCn0XOhm3pfHlmaCAWrOSGuQs3NCNlFRjwmVRPY5FJrKYjH3FrLrLdU07zdViAix8C4LxVrRrMB6ligZC3CoDhFA4vMjiPU5SBRqRW4lwVnvMZEZbf0AYbBc2ymnKAOWbQwt2ldiI2qL0aLoL6YtSFUhpwMOR3LP1feUq6XRO5xc9V02nEt9MRQsl5MgmKMcXap4HqAN0yATpjAGRnWqEnE7E1XZg95cEl2gO4HXejKzR0kiTUudcw6P4t1RYLRx7isZNJxiq1JZz6FpEe7QhwGbhPySNMbXJtmYuhAaTpfGdGKMxvHHB9LmELOChdyfjHMwMZ2B0xgU2eJgJimCwLH3UEmExgAwJDD4GSCqevYAMK4P9FKPl0dku0KZ7uOJ8oNloEsrbvMuhuKFDuO1PNvxtdCcgASzNVzdueOtUm1giZIDqbb6j11nqi9NoFeck1zZi2kfGF7OeUp4vYszuhQNi4vd03QeVAduM9h9v36Nz1YobRxB2CjTp6qdKdW9IYBp8aExZpipnJIbfD2hTWE44kIu7Q17f4C9kycGjsLwAWkVbfTRmBMU8SbVKV1EJTrN1gGqGX7quSwg1Vp4qslKAk6EIkoReIl5DuzuH8Rbvrkp5LFFAhNhb1hvXvVWcibtDjQSradNtuYzGf2AAduhxOTnZjzbsceGYhQA5a5NtqxE2GBlW8CPoPzIyfMfPjdAIUmAcns7Fkp44nju2htwhryUyidEzDVyTwevquARjt5a7eu8qIKfPrYgbOAlPgA1JHNi55ivTNpDuQ8drNiafZIntA43HI447WtITYYvLxFRG8OWvJRwI0N7dvHYO8H8lYI1OwatfvLKlJqjtdJBBvMWXdT4SbxHUdNTDUQmqFGZaLx1AvYPnJTYRzrqn5ZnXyWQ1ZCwtvZK209TxoezJ2sGorE46C7Zyki6EcXlX2A8upUUh9IhqLYTzidIRrAPE5mZmosyDyShjnRiN5CLXZAI21eV4v3a6WXI8TKkUk3fhhajOgPXshlyCEfDAyESpz1J8RECu6vQs81E1ZNE5ha5UGw2wk3Ea8oSTfqTiu0OeisV2a6bfldvW4x0OL8PS57uuY0v0OZPSUPWmPQgnmJRVw8vmh62bpFekMnUH7y31fXU6MIyZaiBs1FEu7qF6irBszHt2ARy50SjgGwQZWcecgvB8gB874g3ES9mZer3diYGF3Wssmsm6XRdsNcuNn3yzuoi52cRrBYUOISegTBVApn4zfuCC9Y4AAfe6wmmiuN8hL6KJeOjrdK5EFQHGyrzeuIMaT3B2nKz1PNONVQ0udbqCQebz3cq7NPe6kGKFLiE6euWjdoMuAbuu8rTkAa42ensXz4a1Yo450ZVgYypaDtepDQWFkJyTHDW1HTVZfCok0tp7STRiQ8n3NKxOUSL9veuTsDs1FaV2rbzR3DvkEJrhJ10Rm0pvLgui5GUDKyWLnrqcNVtOIzFaj9K5pwMfnREm1VIs84ePX0GsMjirfOfubzDoYjavbiCtTB86nKx0tfCKtl0yUQ5PWSBqdGASY3mr5hZcFZ9bA6uXXGTNqMpUH3gqxCoF6t2yAim93t77jYkiFt3OBlBRVQzRsPbgEKRXbX3bWQj6NpDzNCQPYTs45HsQB967f4yByzLH8X289YAZJhJJyFTMCLbpdKFuMBX5Msyr4d15sBa1h5bI13dqU14WBnMKD12LkHMjHiyde6xf5EELf082sUfiAZaROFuDCDnA89p6y6oYEUgF1L9yQElZO4R6IrkJsEFN9hvARf3CH4ENqbYxtUN9gsB9CLCGKMy2R4wGKU3Dkyea27YCR4QHCdqX3HqOpy12uxBANvbrfEro9q5NJrGK7WVq3nNabN05x4TmIZk3asc8ehvDyhSgQLY0wwyvrkcYqNiETybJ57RjwVg1YE0IZEBfyAUNXE4goc2jtbZbHfcpTzt08pSJQZTAzuxrdQLS4EnaFHPpMdPh1YXUdclj6g2sjYbhoTYcV97bVDAUztMZ4EarUcv6tgQOvK66RmJCF2zVEpFDBS6AVZJWzrVlnuiweXpH0L9eY2Wy2EuAHi7gL4o0i0AkOapqY1TPUWUwBaVrKQzkL8QQbczgc97pMvSnGYMlcSdzlamFtUmRoOPmhBGMpVqmcxnstnqJ0TXMV65zbRN2hk3YVF5HwPjuWJmfkVYnyazuqKuaaohrQIe7YOOSAmD7C2vDnI50y1oScQqIPb87QAmguFz7jfNBSPymjPJ7UrToaJen7LEQr8S2b69ayZYNIyWbcpaW5ACUqdyT5AeHYhdENORnWS2B17qnBPtyvb4WujJCafLmsMFhQbcGonDZkHEOAnOcwRwJ4KIPr4MlQLRKsdnurPDDEmpCtCnFg8vPObOPHoHgICb9j35pG1YNhAAGIGTZ4g3JTJzFvTcW7GDRxREPZffKOuQTJoMYYaaPwnE0SainEpCFAukJbDy1ss5cZt60nqTw1asLzwMKJu5PHpU9sB9YN7J2cPhIbfb4387zSmSvqbt3I8NFjDbuYEhe6nZ7gRT5Th0W0MoyzHlmy4MSXbaAfUJNsLQJmdhdVKDsqMz0aXKIVNsXtn88owrhw0yqxU0K3IfTothafhpQ8daRUnbjzULViWRvUz7dI1N3GgylRzaEXQPgbj0DQ7RujNTcJoSp7I1ELjFFSBZDm4Jx5eXq0aS2SKJPFX7XmFfkkR99wRiHx4ByVTL5umojRhY5j8vg3l3yfliJbeOTXckaYiezrucuHaiVFWR2kjk9PUm57bDpvtSFMic652iDufj4hqpy5MH5r2lg67T6Bbb3fcq49cVJ3hkN2GfRqVhoPxmHyvotu5koheVh7oHDaLaf4VvcQMd5MF8sicaX3GXfoLjlfFZwfJBpXNbbVemD7XghpIEwuFjA1USU8yJnTdvCJ2bFmPNWFeWsBVDyl7XUsbgB3K2zz806xODZT639dqiqhGXQNbgYtShikQhiHhZF4wf4IY588LE4EO2bdXBb2Wezm8Gl2J5GAfqnx5Z6NF7h1gGkM27hpnmKNylKZjqTNANj0CRU4awpdVrYGX7hT0u452Y5bXpVl7cLuK7j2k7VG93NXPsXADhQA8R9WDcpU0PLzFWFq1omoQ9ZRSlvh8R4pRp4vHIYf4A5uQEmv5Owr4pFQcWdp5GAdkpBaSHvUhvMxOSpsqVB2LHvvs1RiOUHHhHdZEKpX25mK9moud8pKT4efru1SlRRSsxdz87hTJMUrueydHDPXbo9AvExctdqxuCk03Fy8cB57qrkQQ50oGNuTNPColMrwVfmuTt81uSZremLbINILnCVXEnvTugRQfFYMnprqMB4mVJfZfh6XVLdOyW4BPaFrBsZGFy7udoWJwE8ACx4UpJW6m1ltckofzA6AUxzXprXDCCL118m8bBB2hzDKmqeLk5ZYKsLROkTqRAxmJjBSZSo2XBroO5rVvkOZrOZRe8NgaHFMLPn0I6hsqwA7VdKlpbqknax84iWrtBe8ErxgPIQeYhELyK1deW1YWBagD21MBTc2h5LliIlglZg41H8Zl3GvUv0XNZegR5bx1kiM9WFGV9Yt37iQQGquWAMKCAb6AqpkCtKs7sXKaEAVsbh32tlkAg4ngspjwzYHTPYKUuigPX5K8siUfaAW9WJl7r8dc4ju97osWETOcBENLsfwB66TvsttORtOedylnErplZP3hjt7o39JllXDobj3l10bSr4B09eYVWi2DLGavYktKSKj1PrqzuGUaqcFxqoebpuDEAx5vl8ZmSYrmS2RBJ1n2s3lkKdaVWTmfIXlyMMT7Ac3lCXpGNnpf8ccTffv3E0fBrpCSpVc48dM5e5iTpRPrfWxAjrud9jSrqVBXsw3pqUvhuVmBpmwoKAfQGxHrauna3f48AFefGDozxXXjpdM9ZDWHsRUBTFNzDs8tUATtegSzZfNJCS9k0p5q2cueyU1mtwMJIdf0FrsVGiAyX7PFkWvLHi29fpprZQd0gbMMw2Bt10ZbZCsjPX261cXmVa6ZPnkVQm2w1ory3uWejuq20oQCyXTYyv1Ki4tbdPxoNn04Je7uS3QHDCsUl4i9zKNhBJ3g55bhIZWfwmLi3S7oY16gImdC6vvjsMKkCPzXv4pPaVhHH7o4f0mWEz30k4o7GQNOUy8LPM3NmlZF7QaIBdRfozG86jwQkC3jTNR357pdPjOqMERtIS4WEJBgbaeUCu5MOhsNdaD91iCeghIpOECFyTdEkUCGPPCIAtuAOKBdhPu40UxHx30dELMTK3azHOuOnLTsdiM4KJ9yF4Ab2eiz5j2T95sDx3aiEJDVDPCa55hO0XTBM9OSNtdzjdTdZT19XrwD0wPWZcBhfJ66X1uNM2eud1btzglqZP52qqYU7BK2M3BBZKKjy7P6YzmgaPHWnFGHZdwdz3Yq6e3N76Cjkfl8Sy0mkwd6pt0geDM1jNNZrcT8dUfLLaiUqcZm1KRVdpZaBrboDSuCxfWYlxqgsldwlGL4C06ceFUDXX8PzxzWEgOd8OU4F22pcNJOnwJGo6rYA3tvhAuq2WKVg6tgFCb1p7dzF4Ke3J0dv3IneMSNnHG4hkvxW6VzIykDUtYEjMQO35tdnEA0vMVLXIahpJpz4HGs5wwRgoZx1e1zD1pXi7KmEVTlfattgcGFlKjZJ60fEdloZEmiXodxT63CzuJHnjHDOL8qcMzTxHb8OCainga4w1fk4uILLAWqmTFpDcFGSF5lbOFUwhvtMK6knIWZ8ZApZvTGBt1qv3xKUJqPcWiweI4kk57zgyTPZku2mg4fJWDKSfiRSi7LvtpKkdqjein9lP7LMv5lKutprVzjmvHBPjunXGqakWx39xYH8RD6qF3Fw2BnIIesiicZsDv69Ggbu9Y334UeFPNIJ3LGp2I8xcUxlP5dJAh4V05p1HvIZ5Fhk0oCWlvNXdLqzbVsbfW9jWyQTaZXzw7WT3rqFQc7wvw4ayp5eKmUclqB1yOvrI14XGhmH7QMaAYNTIE2RHjYXVgvbmFRi0oB1v4nDEeSTn3KHBRQD8TilCagKg0XYPj2eAgWs12ZRYzlGyCvYZ1pol5wAwc9AFFGwsTJ9UYkbxlZv7wKDx7nFzlUSMC1kMvS2ECwvHzSycqHPRwCGipvG6kWz0mGvASXeKjm47iMROoY0MRK0uvgNdTTOTdxkMgOuCDIlxfit5QKjyzaVAg2kDwENfSd6XPMgSprTSLuNDXdg5NHCwUvDbEHVxpMgOItZymPZtPweOrnPdlEB4UwLZ8jqtShi5oDYvhkh85FwwT25OHFvDUWTTCV5n73pQ8kLo8zsB3mbWfGwg62guj3C50Dh42fAZEPBRSHDRTg3r0z39Vyj490lk2UpZeNyylwuEKmuIqEkbE3BRT2YEjTM8a2PU5grCuzculibcoRUpb1sIQiMRTf4wrtT1CnKcoUJ1T28DC04dTJVRcm3w3WzNLdrnovkX6NahblTzDvq5eXkoEaZv6HClmGuho4FH6s6i0OdmmW8qkNOnk7BhexiyAd3UYERlFwvZ6LP55tFOc3vnlhyylx1rTTgu1NFljRNs7rGiT7SnGFaFK7GITEZFEYI7DmOEUZXxDSHjYuOVN0YAJP2cZFgagyMwGJdrpH8S7cewYPMKz2Go2GBKl1OA6pJ8T91tUdEcGVg9JCMQUA4sBtlIuRTVV3cduIhsLCTi2ewItkh9MRP1kevVa9WcXejQQKreZmq5EZtzThW71r7E2tcvwFeqiwv3JZnV16bZ7NwZT6uvSrOnIFUyMsxhh8xCkVY82VLTAZhPXB8t6CbyjZ5stos6WmNZgoEsD8GU8pmzSTubAqQXkTbiODF2pePe6S9uQ9HngGGBnOjY4QUcAcScDsfflyXVqyxgTelGD4vXoba6qRWCqc9LKpyk4jCKYvLX9tzXusO7bhT2KRvF4MObDqdE4KnCCIF3zeVD0vImR20MmRTBHRCNm3s6GfyeTYEAlW3L2igZJ7Myj5zGLccMt2EohGc38HfWZ4mlvXRLHKB233PyKALYifqlAxTXaWUk13o6nACQDvN7DxSCA0daJeuznK1Dr52bC4IXCTahK1An6LkQMfsXb7Qus6ey241Vb4wTgFHqsdCx7qPxeAghmsTOHRVl\")\n    ],\n    \"body\": ''\n}\n"
  },
  {
    "path": "tests/requests/valid/027.http",
    "content": "GET /à%20k HTTP/1.0\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/valid/027.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nrequest = {\n    \"method\": \"GET\",\n    \"uri\": uri(\"/\\xc3\\xa0%20k\"),\n    \"version\": (1, 0),\n    \"headers\": [\n    ],\n    \"body\": ''\n}\n"
  },
  {
    "path": "tests/requests/valid/028.http",
    "content": "GET /stuff/here?foo=bar HTTP/1.1\\r\\n\nContent-Length : 3\\r\\n\n\\r\\n\nxyz"
  },
  {
    "path": "tests/requests/valid/028.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\n\ncfg = Config()\ncfg.set(\"strip_header_spaces\", True)\n\nrequest = {\n    \"method\": \"GET\",\n    \"uri\": uri(\"/stuff/here?foo=bar\"),\n    \"version\": (1, 1),\n    \"headers\": [\n        (\"CONTENT-LENGTH\", \"3\"),\n    ],\n    \"body\": b\"xyz\"\n}"
  },
  {
    "path": "tests/requests/valid/029.http",
    "content": "GET /stuff/here?foo=bar HTTP/1.1\\r\\n\nTransfer-Encoding: identity\\r\\n\nTransfer-Encoding: chunked\\r\\n\n\\r\\n\n5\\r\\n\nhello\\r\\n\n000\\r\\n\n"
  },
  {
    "path": "tests/requests/valid/029.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\n\ncfg = Config()\n\nrequest = {\n    \"method\": \"GET\",\n    \"uri\": uri(\"/stuff/here?foo=bar\"),\n    \"version\": (1, 1),\n    \"headers\": [\n        ('TRANSFER-ENCODING', 'identity'),\n        ('TRANSFER-ENCODING', 'chunked'),\n    ],\n    \"body\": b\"hello\"\n}\n"
  },
  {
    "path": "tests/requests/valid/030.http",
    "content": "GET /stuff/here?foo=bar HTTP/1.1\\r\\n\nTransfer-Encoding: identity\\r\\n\nTransfer-Encoding: chunked\\r\\n\n\\r\\n\n5\\r\\n\nhello\\r\\n\n000\\r\\n\n"
  },
  {
    "path": "tests/requests/valid/030.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\n\ncfg = Config()\n\nrequest = {\n    \"method\": \"GET\",\n    \"uri\": uri(\"/stuff/here?foo=bar\"),\n    \"version\": (1, 1),\n    \"headers\": [\n        ('TRANSFER-ENCODING', 'identity'),\n        ('TRANSFER-ENCODING', 'chunked')\n    ],\n    \"body\": b\"hello\"\n}\n"
  },
  {
    "path": "tests/requests/valid/031.http",
    "content": "-BLARGH /foo HTTP/1.1\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/valid/031.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nrequest = {\n    \"method\": \"-BLARGH\",\n    \"uri\": uri(\"/foo\"),\n    \"version\": (1, 1),\n    \"headers\": [],\n    \"body\": b\"\"\n}\n"
  },
  {
    "path": "tests/requests/valid/031compat.http",
    "content": "-blargh /foo HTTP/1.1\\r\\n\n\\r\\n"
  },
  {
    "path": "tests/requests/valid/031compat.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\n\ncfg = Config()\ncfg.set(\"permit_unconventional_http_method\", True)\ncfg.set(\"casefold_http_method\", True)\n\nrequest = {\n    \"method\": \"-BLARGH\",\n    \"uri\": uri(\"/foo\"),\n    \"version\": (1, 1),\n    \"headers\": [],\n    \"body\": b\"\"\n}\n"
  },
  {
    "path": "tests/requests/valid/031compat2.http",
    "content": "-blargh /foo HTTP/1.1\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/valid/031compat2.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\n\ncfg = Config()\ncfg.set(\"permit_unconventional_http_method\", True)\n\nrequest = {\n    \"method\": \"-blargh\",\n    \"uri\": uri(\"/foo\"),\n    \"version\": (1, 1),\n    \"headers\": [],\n    \"body\": b\"\"\n}\n"
  },
  {
    "path": "tests/requests/valid/040.http",
    "content": "GET /keep/same/as?invalid/040 HTTP/1.0\\r\\n\nTransfer_Encoding: tricked\\r\\n\nContent-Length: 7\\r\\n\nContent_Length: -1E23\\r\\n\n\\r\\n\ntricked\\r\\n\n"
  },
  {
    "path": "tests/requests/valid/040.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nrequest = {\n    \"method\": \"GET\",\n    \"uri\": uri(\"/keep/same/as?invalid/040\"),\n    \"version\": (1, 0),\n    \"headers\": [\n        (\"CONTENT-LENGTH\", \"7\")\n    ],\n    \"body\": b'tricked'\n}\n"
  },
  {
    "path": "tests/requests/valid/040_compat.http",
    "content": "GET /keep/same/as?invalid/040 HTTP/1.0\\r\\n\nTransfer_Encoding: tricked\\r\\n\nContent-Length: 7\\r\\n\nContent_Length: -1E23\\r\\n\n\\r\\n\ntricked\\r\\n\n"
  },
  {
    "path": "tests/requests/valid/040_compat.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\n\ncfg = Config()\ncfg.set(\"header_map\", \"dangerous\")\n\nrequest = {\n    \"method\": \"GET\",\n    \"uri\": uri(\"/keep/same/as?invalid/040\"),\n    \"version\": (1, 0),\n    \"headers\": [\n        (\"TRANSFER_ENCODING\", \"tricked\"),\n        (\"CONTENT-LENGTH\", \"7\"),\n        (\"CONTENT_LENGTH\", \"-1E23\"),\n    ],\n    \"body\": b'tricked'\n}\n"
  },
  {
    "path": "tests/requests/valid/099.http",
    "content": "POST /test-form HTTP/1.1\\r\\n\nHost: 0.0.0.0:5000\\r\\n\nUser-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64; rv:25.0) Gecko/20100101 Firefox/25.0\\r\\n\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\\r\\n\nAccept-Language: en-us,en;q=0.7,el;q=0.3\\r\\n\nAccept-Encoding: gzip, deflate\\r\\n\nCookie: csrftoken=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX; sessionid=YYYYYYYYYYYYYYYYYYYYYYYYYYYY\\r\\n\nConnection: keep-alive\\r\\n\nContent-Type: multipart/form-data; boundary=---------------------------320761477111544\\r\\n\nContent-Length: 17914\\r\\n\n\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"csrfmiddlewaretoken\"\\r\\n\n\\r\\n\nXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"_save\"\\r\\n\n\\r\\n\nSave\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"name\"\\r\\n\n\\r\\n\ntest.example.org\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"type\"\\r\\n\n\\r\\n\nNATIVE\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"master\"\\r\\n\n\\r\\n\n\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_dynamiczone_domain-TOTAL_FORMS\"\\r\\n\n\\r\\n\n1\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_dynamiczone_domain-INITIAL_FORMS\"\\r\\n\n\\r\\n\n1\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_dynamiczone_domain-MAX_NUM_FORMS\"\\r\\n\n\\r\\n\n1\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_dynamiczone_domain-0-is_dynamic\"\\r\\n\n\\r\\n\non\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_dynamiczone_domain-0-id\"\\r\\n\n\\r\\n\n1\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_dynamiczone_domain-0-domain\"\\r\\n\n\\r\\n\n2\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_dynamiczone_domain-__prefix__-id\"\\r\\n\n\\r\\n\n\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_dynamiczone_domain-__prefix__-domain\"\\r\\n\n\\r\\n\n2\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-TOTAL_FORMS\"\\r\\n\n\\r\\n\n1\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-INITIAL_FORMS\"\\r\\n\n\\r\\n\n1\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-MAX_NUM_FORMS\"\\r\\n\n\\r\\n\n1\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-0-ttl\"\\r\\n\n\\r\\n\n3600\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-0-primary\"\\r\\n\n\\r\\n\nns.example.org\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-0-hostmaster\"\\r\\n\n\\r\\n\nhostmaster.test.example.org\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-0-serial\"\\r\\n\n\\r\\n\n2013121701\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-0-refresh\"\\r\\n\n\\r\\n\n10800\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-0-retry\"\\r\\n\n\\r\\n\n3600\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-0-expire\"\\r\\n\n\\r\\n\n604800\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-0-default_ttl\"\\r\\n\n\\r\\n\n3600\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-0-id\"\\r\\n\n\\r\\n\n16\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-0-domain\"\\r\\n\n\\r\\n\n2\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-__prefix__-ttl\"\\r\\n\n\\r\\n\n\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-__prefix__-primary\"\\r\\n\n\\r\\n\n\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-__prefix__-hostmaster\"\\r\\n\n\\r\\n\n\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-__prefix__-serial\"\\r\\n\n\\r\\n\n1\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-__prefix__-refresh\"\\r\\n\n\\r\\n\n10800\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-__prefix__-retry\"\\r\\n\n\\r\\n\n3600\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-__prefix__-expire\"\\r\\n\n\\r\\n\n604800\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-__prefix__-default_ttl\"\\r\\n\n\\r\\n\n3600\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-__prefix__-id\"\\r\\n\n\\r\\n\n\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-__prefix__-domain\"\\r\\n\n\\r\\n\n2\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-2-TOTAL_FORMS\"\\r\\n\n\\r\\n\n0\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-2-INITIAL_FORMS\"\\r\\n\n\\r\\n\n0\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-2-MAX_NUM_FORMS\"\\r\\n\n\\r\\n\n1000\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-2-__prefix__-id\"\\r\\n\n\\r\\n\n\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-2-__prefix__-domain\"\\r\\n\n\\r\\n\n2\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-2-__prefix__-name\"\\r\\n\n\\r\\n\n\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-2-__prefix__-ttl\"\\r\\n\n\\r\\n\n\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-2-__prefix__-content\"\\r\\n\n\\r\\n\n\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-3-TOTAL_FORMS\"\\r\\n\n\\r\\n\n0\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-3-INITIAL_FORMS\"\\r\\n\n\\r\\n\n0\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-3-MAX_NUM_FORMS\"\\r\\n\n\\r\\n\n1000\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-3-__prefix__-id\"\\r\\n\n\\r\\n\n\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-3-__prefix__-domain\"\\r\\n\n\\r\\n\n2\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-3-__prefix__-name\"\\r\\n\n\\r\\n\n\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-3-__prefix__-ttl\"\\r\\n\n\\r\\n\n\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-3-__prefix__-prio\"\\r\\n\n\\r\\n\n\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-3-__prefix__-content\"\\r\\n\n\\r\\n\n\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-4-TOTAL_FORMS\"\\r\\n\n\\r\\n\n0\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-4-INITIAL_FORMS\"\\r\\n\n\\r\\n\n0\\r\\n\n---------------------\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-5-TOTAL_FORMS\"\\r\\n\n\\r\\n\n0\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-5-INITIAL_FORMS\"\\r\\n\n\\r\\n\n0\\r\\n\n---------------------\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-6-TOTAL_FORMS\"\\r\\n\n\\r\\n\n0\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-6-INITIAL_FORMS\"\\r\\n\n\\r\\n\n0\\r\\n\n---------------------\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-7-TOTAL_FORMS\"\\r\\n\n\\r\\n\n0\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-7-INITIAL_FORMS\"\\r\\n\n\\r\\n\n0\\r\\n\n---------------------\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-8-TOTAL_FORMS\"\\r\\n\n\\r\\n\n0\\r\\n\n-----------------------------320761477111544\\r\\n\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-8-INITIAL_FORMS\"\\r\\n\n\\r\\n\n0\\r\\n\n---------------------\\r\\n"
  },
  {
    "path": "tests/requests/valid/099.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nrequest = {\n    \"method\": \"POST\",\n    \"uri\": uri(\"/test-form\"),\n    \"version\": (1, 1),\n    \"headers\": [\n        (\"HOST\", \"0.0.0.0:5000\"),\n        (\"USER-AGENT\", \"Mozilla/5.0 (Windows NT 6.2; WOW64; rv:25.0) Gecko/20100101 Firefox/25.0\"),\n        (\"ACCEPT\", \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\"),\n        (\"ACCEPT-LANGUAGE\", \"en-us,en;q=0.7,el;q=0.3\"),\n        (\"ACCEPT-ENCODING\", \"gzip, deflate\"),\n        (\"COOKIE\", \"csrftoken=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX; sessionid=YYYYYYYYYYYYYYYYYYYYYYYYYYYY\"),\n        (\"CONNECTION\", \"keep-alive\"),\n        (\"CONTENT-TYPE\", \"multipart/form-data; boundary=---------------------------320761477111544\"),\n        (\"CONTENT-LENGTH\", \"17914\"),\n    ],\n    \"body\": b\"\"\"-----------------------------320761477111544\nContent-Disposition: form-data; name=\"csrfmiddlewaretoken\"\n\nXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"_save\"\n\nSave\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"name\"\n\ntest.example.org\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"type\"\n\nNATIVE\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"master\"\n\n\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_dynamiczone_domain-TOTAL_FORMS\"\n\n1\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_dynamiczone_domain-INITIAL_FORMS\"\n\n1\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_dynamiczone_domain-MAX_NUM_FORMS\"\n\n1\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_dynamiczone_domain-0-is_dynamic\"\n\non\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_dynamiczone_domain-0-id\"\n\n1\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_dynamiczone_domain-0-domain\"\n\n2\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_dynamiczone_domain-__prefix__-id\"\n\n\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_dynamiczone_domain-__prefix__-domain\"\n\n2\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-TOTAL_FORMS\"\n\n1\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-INITIAL_FORMS\"\n\n1\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-MAX_NUM_FORMS\"\n\n1\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-0-ttl\"\n\n3600\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-0-primary\"\n\nns.example.org\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-0-hostmaster\"\n\nhostmaster.test.example.org\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-0-serial\"\n\n2013121701\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-0-refresh\"\n\n10800\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-0-retry\"\n\n3600\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-0-expire\"\n\n604800\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-0-default_ttl\"\n\n3600\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-0-id\"\n\n16\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-0-domain\"\n\n2\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-__prefix__-ttl\"\n\n\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-__prefix__-primary\"\n\n\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-__prefix__-hostmaster\"\n\n\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-__prefix__-serial\"\n\n1\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-__prefix__-refresh\"\n\n10800\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-__prefix__-retry\"\n\n3600\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-__prefix__-expire\"\n\n604800\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-__prefix__-default_ttl\"\n\n3600\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-__prefix__-id\"\n\n\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-__prefix__-domain\"\n\n2\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-2-TOTAL_FORMS\"\n\n0\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-2-INITIAL_FORMS\"\n\n0\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-2-MAX_NUM_FORMS\"\n\n1000\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-2-__prefix__-id\"\n\n\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-2-__prefix__-domain\"\n\n2\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-2-__prefix__-name\"\n\n\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-2-__prefix__-ttl\"\n\n\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-2-__prefix__-content\"\n\n\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-3-TOTAL_FORMS\"\n\n0\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-3-INITIAL_FORMS\"\n\n0\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-3-MAX_NUM_FORMS\"\n\n1000\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-3-__prefix__-id\"\n\n\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-3-__prefix__-domain\"\n\n2\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-3-__prefix__-name\"\n\n\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-3-__prefix__-ttl\"\n\n\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-3-__prefix__-prio\"\n\n\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-3-__prefix__-content\"\n\n\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-4-TOTAL_FORMS\"\n\n0\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-4-INITIAL_FORMS\"\n\n0\n---------------------\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-5-TOTAL_FORMS\"\n\n0\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-5-INITIAL_FORMS\"\n\n0\n---------------------\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-6-TOTAL_FORMS\"\n\n0\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-6-INITIAL_FORMS\"\n\n0\n---------------------\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-7-TOTAL_FORMS\"\n\n0\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-7-INITIAL_FORMS\"\n\n0\n---------------------\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-8-TOTAL_FORMS\"\n\n0\n-----------------------------320761477111544\nContent-Disposition: form-data; name=\"foobar_manager_record_domain-8-INITIAL_FORMS\"\n\n0\n---------------------\n\"\"\".decode('utf-8').replace('\\n', '\\r\\n').encode('utf-8'),\n}\n"
  },
  {
    "path": "tests/requests/valid/100.http",
    "content": "GET ///keeping_slashes HTTP/1.1\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/valid/100.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nrequest = {\n    \"method\": \"GET\",\n    \"uri\": uri(\"///keeping_slashes\"),\n    \"version\": (1, 1),\n    \"headers\": [],\n    \"body\": b\"\"\n}\n"
  },
  {
    "path": "tests/requests/valid/compat_obs_fold.http",
    "content": "GET / HTTP/1.1\\r\\n\nLong: one\\r\\n\n two\\r\\n\nHost: localhost\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/valid/compat_obs_fold.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.http.errors import ObsoleteFolding\nfrom gunicorn.config import Config\n\ncfg = Config()\ncfg.set('permit_obsolete_folding', True)\n\nrequest = {\n    \"method\": \"GET\",\n    \"uri\": uri(\"/\"),\n    \"version\": (1, 1),\n    \"headers\": [\n        (\"LONG\", \"one two\"),\n        (\"HOST\", \"localhost\"),\n    ],\n    \"body\": b\"\"\n}\n"
  },
  {
    "path": "tests/requests/valid/compat_obs_fold_huge.http",
    "content": "GET / HTTP/1.1\\r\\n\nX-SSL-Cert:    -----BEGIN CERTIFICATE-----\\r\\n\n    MIIFbTCCBFWgAwIBAgICH4cwDQYJKoZIhvcNAQEFBQAwcDELMAkGA1UEBhMCVUsx\\r\\n\n    ETAPBgNVBAoTCGVTY2llbmNlMRIwEAYDVQQLEwlBdXRob3JpdHkxCzAJBgNVBAMT\\r\\n\n    AkNBMS0wKwYJKoZIhvcNAQkBFh5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMu\\r\\n\n    dWswHhcNMDYwNzI3MTQxMzI4WhcNMDcwNzI3MTQxMzI4WjBbMQswCQYDVQQGEwJV\\r\\n\n    SzERMA8GA1UEChMIZVNjaWVuY2UxEzARBgNVBAsTCk1hbmNoZXN0ZXIxCzAJBgNV\\r\\n\n    BAcTmrsogriqMWLAk1DMRcwFQYDVQQDEw5taWNoYWVsIHBhcmQYJKoZIhvcNAQEB\\r\\n\n    BQADggEPADCCAQoCggEBANPEQBgl1IaKdSS1TbhF3hEXSl72G9J+WC/1R64fAcEF\\r\\n\n    W51rEyFYiIeZGx/BVzwXbeBoNUK41OK65sxGuflMo5gLflbwJtHBRIEKAfVVp3YR\\r\\n\n    gW7cMA/s/XKgL1GEC7rQw8lIZT8RApukCGqOVHSi/F1SiFlPDxuDfmdiNzL31+sL\\r\\n\n    0iwHDdNkGjy5pyBSB8Y79dsSJtCW/iaLB0/n8Sj7HgvvZJ7x0fr+RQjYOUUfrePP\\r\\n\n    u2MSpFyf+9BbC/aXgaZuiCvSR+8Snv3xApQY+fULK/xY8h8Ua51iXoQ5jrgu2SqR\\r\\n\n    wgA7BUi3G8LFzMBl8FRCDYGUDy7M6QaHXx1ZWIPWNKsCAwEAAaOCAiQwggIgMAwG\\r\\n\n    1UdEwEB/wQCMAAwEQYJYIZIAYb4QgHTTPAQDAgWgMA4GA1UdDwEB/wQEAwID6DAs\\r\\n\n    BglghkgBhvhCAQ0EHxYdVUsgZS1TY2llbmNlIFVzZXIgQ2VydGlmaWNhdGUwHQYD\\r\\n\n    VR0OBBYEFDTt/sf9PeMaZDHkUIldrDYMNTBZMIGaBgNVHSMEgZIwgY+AFAI4qxGj\\r\\n\n    loCLDdMVKwiljjDastqooXSkcjBwMQswCQYDVQQGEwJVSzERMA8GA1UEChMIZVNj\\r\\n\n    aWVuY2UxEjAQBgNVBAsTCUF1dGhvcml0eTELMAkGA1UEAxMCQ0ExLTArBgkqhkiG\\r\\n\n    9w0BCQEWHmNhLW9wZXJhdG9yQGdyaWQtc3VwcG9ydC5hYy51a4IBADApBgNVHRIE\\r\\n\n    IjAggR5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMudWswGQYDVR0gBBIwEDAO\\r\\n\n    BgwrBgEEAdkvAQEBAQYwPQYJYIZIAYb4QgEEBDAWLmh0dHA6Ly9jYS5ncmlkLXN1\\r\\n\n    cHBvcnQuYWMudmT4sopwqlBWsvcHViL2NybC9jYWNybC5jcmwwPQYJYIZIAYb4Qg\\r\\n\n    EDBDAWLmh0dHA6Ly9jYS5ncmlkLXN1cHBvcnQuYWMudWsvcHViL2NybC9jYWNybC\\r\\n\n    5jcmwwPwYDVR0fBDgwNjA0oDKgMIYuaHR0cDovL2NhLmdyaWQt5hYy51ay9wdWIv\\r\\n\n    Y3JsL2NhY3JsLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAS/U4iiooBENGW/Hwmmd3\\r\\n\n    XCy6Zrt08YjKCzGNjorT98g8uGsqYjSxv/hmi0qlnlHs+k/3Iobc3LjS5AMYr5L8\\r\\n\n    UO7OSkgFFlLHQyC9JzPfmLCAugvzEbyv4Olnsr8hbxF1MbKZoQxUZtMVu29wjfXk\\r\\n\n    hTeApBv7eaKCWpSp7MCbvgzm74izKhu3vlDk9w6qVrxePfGgpKPqfHiOoGhFnbTK\\r\\n\n    wTC6o2xq5y0qZ03JonF7OJspEd3I5zKY3E+ov7/ZhW6DqT8UFvsAdjvQbXyhV8Eu\\r\\n\n    Yhixw1aKEPzNjNowuIseVogKOLXxWI5vAi5HgXdS0/ES5gDGsABo4fqovUKlgop3\\r\\n\n    RA==\\r\\n\n    -----END CERTIFICATE-----\\r\\n\n\\r\\n"
  },
  {
    "path": "tests/requests/valid/compat_obs_fold_huge.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\n\ncfg = Config()\ncfg.set('permit_obsolete_folding', True)\n\ncertificate = \"\"\"-----BEGIN CERTIFICATE-----\n MIIFbTCCBFWgAwIBAgICH4cwDQYJKoZIhvcNAQEFBQAwcDELMAkGA1UEBhMCVUsx\n ETAPBgNVBAoTCGVTY2llbmNlMRIwEAYDVQQLEwlBdXRob3JpdHkxCzAJBgNVBAMT\n AkNBMS0wKwYJKoZIhvcNAQkBFh5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMu\n dWswHhcNMDYwNzI3MTQxMzI4WhcNMDcwNzI3MTQxMzI4WjBbMQswCQYDVQQGEwJV\n SzERMA8GA1UEChMIZVNjaWVuY2UxEzARBgNVBAsTCk1hbmNoZXN0ZXIxCzAJBgNV\n BAcTmrsogriqMWLAk1DMRcwFQYDVQQDEw5taWNoYWVsIHBhcmQYJKoZIhvcNAQEB\n BQADggEPADCCAQoCggEBANPEQBgl1IaKdSS1TbhF3hEXSl72G9J+WC/1R64fAcEF\n W51rEyFYiIeZGx/BVzwXbeBoNUK41OK65sxGuflMo5gLflbwJtHBRIEKAfVVp3YR\n gW7cMA/s/XKgL1GEC7rQw8lIZT8RApukCGqOVHSi/F1SiFlPDxuDfmdiNzL31+sL\n 0iwHDdNkGjy5pyBSB8Y79dsSJtCW/iaLB0/n8Sj7HgvvZJ7x0fr+RQjYOUUfrePP\n u2MSpFyf+9BbC/aXgaZuiCvSR+8Snv3xApQY+fULK/xY8h8Ua51iXoQ5jrgu2SqR\n wgA7BUi3G8LFzMBl8FRCDYGUDy7M6QaHXx1ZWIPWNKsCAwEAAaOCAiQwggIgMAwG\n 1UdEwEB/wQCMAAwEQYJYIZIAYb4QgHTTPAQDAgWgMA4GA1UdDwEB/wQEAwID6DAs\n BglghkgBhvhCAQ0EHxYdVUsgZS1TY2llbmNlIFVzZXIgQ2VydGlmaWNhdGUwHQYD\n VR0OBBYEFDTt/sf9PeMaZDHkUIldrDYMNTBZMIGaBgNVHSMEgZIwgY+AFAI4qxGj\n loCLDdMVKwiljjDastqooXSkcjBwMQswCQYDVQQGEwJVSzERMA8GA1UEChMIZVNj\n aWVuY2UxEjAQBgNVBAsTCUF1dGhvcml0eTELMAkGA1UEAxMCQ0ExLTArBgkqhkiG\n 9w0BCQEWHmNhLW9wZXJhdG9yQGdyaWQtc3VwcG9ydC5hYy51a4IBADApBgNVHRIE\n IjAggR5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMudWswGQYDVR0gBBIwEDAO\n BgwrBgEEAdkvAQEBAQYwPQYJYIZIAYb4QgEEBDAWLmh0dHA6Ly9jYS5ncmlkLXN1\n cHBvcnQuYWMudmT4sopwqlBWsvcHViL2NybC9jYWNybC5jcmwwPQYJYIZIAYb4Qg\n EDBDAWLmh0dHA6Ly9jYS5ncmlkLXN1cHBvcnQuYWMudWsvcHViL2NybC9jYWNybC\n 5jcmwwPwYDVR0fBDgwNjA0oDKgMIYuaHR0cDovL2NhLmdyaWQt5hYy51ay9wdWIv\n Y3JsL2NhY3JsLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAS/U4iiooBENGW/Hwmmd3\n XCy6Zrt08YjKCzGNjorT98g8uGsqYjSxv/hmi0qlnlHs+k/3Iobc3LjS5AMYr5L8\n UO7OSkgFFlLHQyC9JzPfmLCAugvzEbyv4Olnsr8hbxF1MbKZoQxUZtMVu29wjfXk\n hTeApBv7eaKCWpSp7MCbvgzm74izKhu3vlDk9w6qVrxePfGgpKPqfHiOoGhFnbTK\n wTC6o2xq5y0qZ03JonF7OJspEd3I5zKY3E+ov7/ZhW6DqT8UFvsAdjvQbXyhV8Eu\n Yhixw1aKEPzNjNowuIseVogKOLXxWI5vAi5HgXdS0/ES5gDGsABo4fqovUKlgop3\n RA==\n -----END CERTIFICATE-----\"\"\".replace(\"\\n\", \"\")\n\nrequest = {\n    \"method\": \"GET\",\n    \"uri\": uri(\"/\"),\n    \"version\": (1, 1),\n    \"headers\": [(\"X-SSL-CERT\", certificate)],\n    \"body\": b\"\"\n}\n"
  },
  {
    "path": "tests/requests/valid/padding_01.http",
    "content": "GET / HTTP/1.1\\r\\n\nHost: localhost\\r\\n\nName: \\t value \\t \\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/valid/padding_01.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\nrequest = {\n    \"method\": \"GET\",\n    \"uri\": uri(\"/\"),\n    \"version\": (1, 1),\n    \"headers\": [\n        (\"HOST\", \"localhost\"),\n        (\"NAME\", \"value\")\n    ],\n    \"body\": b\"\",\n}\n"
  },
  {
    "path": "tests/requests/valid/pp_01.http",
    "content": "PROXY TCP4 192.168.0.1 192.168.0.11 56324 443\\r\\n\nGET /stuff/here?foo=bar HTTP/1.0\\r\\n\nServer: http://127.0.0.1:5984\\r\\n\nContent-Type: application/json\\r\\n\nContent-Length: 14\\r\\n\n\\r\\n\n{\"nom\": \"nom\"}\n"
  },
  {
    "path": "tests/requests/valid/pp_01.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\n\ncfg = Config()\ncfg.set('proxy_protocol', True)\n\nrequest = {\n    \"method\": \"GET\",\n    \"uri\": uri(\"/stuff/here?foo=bar\"),\n    \"version\": (1, 0),\n    \"headers\": [\n        (\"SERVER\", \"http://127.0.0.1:5984\"),\n        (\"CONTENT-TYPE\", \"application/json\"),\n        (\"CONTENT-LENGTH\", \"14\")\n    ],\n    \"body\": b'{\"nom\": \"nom\"}'\n}\n"
  },
  {
    "path": "tests/requests/valid/pp_02.http",
    "content": "PROXY TCP4 192.168.0.1 192.168.0.11 56324 443\\r\\n\nGET /stuff/here?foo=bar HTTP/1.1\\r\\n\nServer: http://127.0.0.1:5984\\r\\n\nContent-Type: application/json\\r\\n\nContent-Length: 14\\r\\n\nConnection: keep-alive\\r\\n\n\\r\\n\n{\"nom\": \"nom\"}\nPOST /post_chunked_all_your_base HTTP/1.1\\r\\n\nTransfer-Encoding: chunked\\r\\n\n\\r\\n\n1e\\r\\n\nall your base are belong to us\\r\\n\n0\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/valid/pp_02.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\n\ncfg = Config()\ncfg.set(\"proxy_protocol\", True)\n\nreq1 = {\n    \"method\": \"GET\",\n    \"uri\": uri(\"/stuff/here?foo=bar\"),\n    \"version\": (1, 1),\n    \"headers\": [\n        (\"SERVER\", \"http://127.0.0.1:5984\"),\n        (\"CONTENT-TYPE\", \"application/json\"),\n        (\"CONTENT-LENGTH\", \"14\"),\n        (\"CONNECTION\", \"keep-alive\")\n    ],\n    \"body\": b'{\"nom\": \"nom\"}'\n}\n\n\nreq2 = {\n    \"method\": \"POST\",\n    \"uri\": uri(\"/post_chunked_all_your_base\"),\n    \"version\": (1, 1),\n    \"headers\": [\n        (\"TRANSFER-ENCODING\", \"chunked\"),\n        ],\n    \"body\": b\"all your base are belong to us\"\n}\n\nrequest = [req1, req2]\n"
  },
  {
    "path": "tests/requests/valid/pp_03.http",
    "content": "GET /no/proxy/header HTTP/1.1\\r\\n\nHost: example.com\\r\\n\nContent-Length: 0\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/valid/pp_03.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\n\ncfg = Config()\ncfg.set(\"proxy_protocol\", True)\n\nrequest = {\n    \"method\": \"GET\",\n    \"uri\": uri(\"/no/proxy/header\"),\n    \"version\": (1, 1),\n    \"headers\": [\n        (\"HOST\", \"example.com\"),\n        (\"CONTENT-LENGTH\", \"0\")\n    ],\n    \"body\": b\"\"\n}\n"
  },
  {
    "path": "tests/requests/valid/pp_04.http",
    "content": "\\x0D\\x0A\\x0D\\x0A\\x00\\x0D\\x0A\\x51\\x55\\x49\\x54\\x0A\\x21\\x11\\x00\\x0C\\xC0\\xA8\\x01\\x0A\\xC0\\xA8\\x01\\x01\\x30\\x39\\x01\\xBBGET /proxy/v2/ipv4 HTTP/1.1\\r\\n\nHost: example.com\\r\\n\nContent-Length: 0\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/valid/pp_04.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\n\ncfg = Config()\ncfg.set(\"proxy_protocol\", True)\n\nrequest = {\n    \"method\": \"GET\",\n    \"uri\": uri(\"/proxy/v2/ipv4\"),\n    \"version\": (1, 1),\n    \"headers\": [\n        (\"HOST\", \"example.com\"),\n        (\"CONTENT-LENGTH\", \"0\")\n    ],\n    \"body\": b\"\"\n}\n"
  },
  {
    "path": "tests/requests/valid/pp_05.http",
    "content": "\\x0D\\x0A\\x0D\\x0A\\x00\\x0D\\x0A\\x51\\x55\\x49\\x54\\x0A\\x21\\x21\\x00\\x24\\x20\\x01\\x0D\\xB8\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x20\\x01\\x0D\\xB8\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\xD4\\x31\\x00\\x50GET /proxy/v2/ipv6 HTTP/1.1\\r\\n\nHost: example.com\\r\\n\nContent-Length: 0\\r\\n\n\\r\\n\n"
  },
  {
    "path": "tests/requests/valid/pp_05.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom gunicorn.config import Config\n\ncfg = Config()\ncfg.set(\"proxy_protocol\", True)\n\nrequest = {\n    \"method\": \"GET\",\n    \"uri\": uri(\"/proxy/v2/ipv6\"),\n    \"version\": (1, 1),\n    \"headers\": [\n        (\"HOST\", \"example.com\"),\n        (\"CONTENT-LENGTH\", \"0\")\n    ],\n    \"body\": b\"\"\n}\n"
  },
  {
    "path": "tests/support.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport functools\nimport sys\nimport unittest\nimport platform\nfrom wsgiref.validate import validator\n\nHOST = \"127.0.0.1\"\n\n\ndef create_app(name=\"World\", count=1):\n    message = (('Hello, %s!\\n' % name) * count).encode(\"utf8\")\n    length = str(len(message))\n\n    @validator\n    def app(environ, start_response):\n        \"\"\"Simplest possible application object\"\"\"\n\n        status = '200 OK'\n\n        response_headers = [\n            ('Content-type', 'text/plain'),\n            ('Content-Length', length),\n        ]\n        start_response(status, response_headers)\n        return iter([message])\n\n    return app\n\n\napp = application = create_app()\nnone_app = None\n\n\ndef error_factory():\n    raise TypeError(\"inner\")\n\n\ndef requires_mac_ver(*min_version):\n    \"\"\"Decorator raising SkipTest if the OS is Mac OS X and the OS X\n    version if less than min_version.\n\n    For example, @requires_mac_ver(10, 5) raises SkipTest if the OS X version\n    is lesser than 10.5.\n    \"\"\"\n    def decorator(func):\n        @functools.wraps(func)\n        def wrapper(*args, **kw):\n            if sys.platform == 'darwin':\n                version_txt = platform.mac_ver()[0]\n                try:\n                    version = tuple(map(int, version_txt.split('.')))\n                except ValueError:\n                    pass\n                else:\n                    if version < min_version:\n                        min_version_txt = '.'.join(map(str, min_version))\n                        raise unittest.SkipTest(\n                            \"Mac OS X %s or higher required, not %s\"\n                            % (min_version_txt, version_txt))\n            return func(*args, **kw)\n        wrapper.min_version = min_version\n        return wrapper\n    return decorator\n"
  },
  {
    "path": "tests/support_dirty_app.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Support module for dirty app tests.\"\"\"\n\nfrom gunicorn.dirty.app import DirtyApp\n\n\nclass TestDirtyApp(DirtyApp):\n    \"\"\"A simple dirty app for testing.\"\"\"\n\n    def __init__(self):\n        self.initialized = False\n        self.closed = False\n        self.data = {}\n\n    def init(self):\n        self.initialized = True\n        self.data['init_called'] = True\n\n    def store(self, key, value):\n        self.data[key] = value\n        return {\"stored\": True, \"key\": key}\n\n    def retrieve(self, key):\n        return self.data.get(key)\n\n    def compute(self, a, b, operation=\"add\"):\n        if operation == \"add\":\n            return a + b\n        elif operation == \"multiply\":\n            return a * b\n        else:\n            raise ValueError(f\"Unknown operation: {operation}\")\n\n    def close(self):\n        self.closed = True\n        self.data.clear()\n\n\nclass BrokenInitApp(DirtyApp):\n    \"\"\"A dirty app that fails during init.\"\"\"\n\n    def init(self):\n        raise RuntimeError(\"Init failed!\")\n\n\nclass BrokenInstantiationApp(DirtyApp):\n    \"\"\"A dirty app that fails during instantiation.\"\"\"\n\n    def __init__(self):\n        raise RuntimeError(\"Cannot instantiate!\")\n\n\nclass NotAClass:\n    \"\"\"Not a class, just an instance for testing.\"\"\"\n    pass\n\n\nnot_a_class = NotAClass()\n\n\nclass MissingCallApp:\n    \"\"\"An invalid dirty app missing __call__.\"\"\"\n\n    def init(self):\n        pass\n\n    def close(self):\n        pass\n\n\nclass SlowDirtyApp(DirtyApp):\n    \"\"\"A dirty app with slow methods for timeout testing.\"\"\"\n\n    def __init__(self):\n        self.initialized = False\n        self.closed = False\n\n    def init(self):\n        self.initialized = True\n\n    def slow_action(self, delay=1.0):\n        \"\"\"An action that takes time to complete.\"\"\"\n        import time\n        time.sleep(delay)\n        return {\"delayed\": True, \"duration\": delay}\n\n    def fast_action(self):\n        \"\"\"A fast action for comparison.\"\"\"\n        return {\"fast\": True}\n\n    def close(self):\n        self.closed = True\n\n\nclass HeavyModelApp(DirtyApp):\n    \"\"\"A dirty app that simulates a heavy model requiring limited workers.\n\n    Uses the workers class attribute to limit how many workers load this app.\n    \"\"\"\n    workers = 2  # Only 2 workers should load this app\n\n    def __init__(self):\n        self.initialized = False\n        self.closed = False\n        self.model_data = None\n        self.worker_id = None\n\n    def init(self):\n        import os\n        self.initialized = True\n        # Store the worker PID to verify which worker handled the request\n        self.worker_id = os.getpid()\n        # Simulate loading a heavy model\n        self.model_data = {\"loaded\": True, \"worker\": self.worker_id}\n\n    def predict(self, data):\n        \"\"\"Simulate model prediction.\"\"\"\n        return {\n            \"prediction\": f\"result_for_{data}\",\n            \"worker_id\": self.worker_id,\n        }\n\n    def get_worker_id(self):\n        \"\"\"Return the worker ID that loaded this app.\"\"\"\n        return self.worker_id\n\n    def close(self):\n        self.closed = True\n        self.model_data = None\n\n\nclass LightweightApp(DirtyApp):\n    \"\"\"A lightweight app that should load on all workers.\"\"\"\n\n    def __init__(self):\n        self.initialized = False\n        self.closed = False\n        self.worker_id = None\n\n    def init(self):\n        import os\n        self.initialized = True\n        self.worker_id = os.getpid()\n\n    def ping(self):\n        \"\"\"Simple ping action.\"\"\"\n        return {\"pong\": True, \"worker_id\": self.worker_id}\n\n    def get_worker_id(self):\n        \"\"\"Return the worker ID that loaded this app.\"\"\"\n        return self.worker_id\n\n    def close(self):\n        self.closed = True\n"
  },
  {
    "path": "tests/support_dirty_apps.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Support module for multi-app dirty tests.\n\nProvides test applications with distinct behaviors for verifying\nthat requests are correctly routed to the appropriate app.\n\"\"\"\n\nfrom gunicorn.dirty.app import DirtyApp\n\n\nclass CounterApp(DirtyApp):\n    \"\"\"App that maintains a counter.\n\n    This app demonstrates stateful behavior where instance variables\n    persist across requests.\n    \"\"\"\n\n    def __init__(self):\n        self.counter = 0\n        self.initialized = False\n        self.closed = False\n\n    def init(self):\n        \"\"\"Initialize the counter app.\"\"\"\n        self.counter = 0\n        self.initialized = True\n\n    def increment(self, amount=1):\n        \"\"\"Increment the counter by the given amount.\n\n        Args:\n            amount: Amount to increment by (default: 1)\n\n        Returns:\n            The new counter value\n        \"\"\"\n        self.counter += amount\n        return self.counter\n\n    def decrement(self, amount=1):\n        \"\"\"Decrement the counter by the given amount.\n\n        Args:\n            amount: Amount to decrement by (default: 1)\n\n        Returns:\n            The new counter value\n        \"\"\"\n        self.counter -= amount\n        return self.counter\n\n    def get_value(self):\n        \"\"\"Get the current counter value.\n\n        Returns:\n            The current counter value\n        \"\"\"\n        return self.counter\n\n    def reset(self):\n        \"\"\"Reset the counter to zero.\n\n        Returns:\n            The counter value (0)\n        \"\"\"\n        self.counter = 0\n        return self.counter\n\n    def close(self):\n        \"\"\"Clean up the counter app.\"\"\"\n        self.closed = True\n        self.counter = 0\n\n\nclass EchoApp(DirtyApp):\n    \"\"\"App that echoes input with a configurable prefix.\n\n    This app demonstrates a different behavior pattern from CounterApp\n    for verifying app routing.\n    \"\"\"\n\n    def __init__(self):\n        self.prefix = \"ECHO:\"\n        self.initialized = False\n        self.closed = False\n        self.echo_count = 0\n\n    def init(self):\n        \"\"\"Initialize the echo app.\"\"\"\n        self.prefix = \"ECHO:\"\n        self.echo_count = 0\n        self.initialized = True\n\n    def echo(self, message):\n        \"\"\"Echo a message with the current prefix.\n\n        Args:\n            message: The message to echo\n\n        Returns:\n            The prefixed message\n        \"\"\"\n        self.echo_count += 1\n        return f\"{self.prefix} {message}\"\n\n    def set_prefix(self, prefix):\n        \"\"\"Set a new prefix for echo messages.\n\n        Args:\n            prefix: The new prefix to use\n\n        Returns:\n            The new prefix\n        \"\"\"\n        self.prefix = prefix\n        return prefix\n\n    def get_prefix(self):\n        \"\"\"Get the current prefix.\n\n        Returns:\n            The current prefix\n        \"\"\"\n        return self.prefix\n\n    def get_echo_count(self):\n        \"\"\"Get the number of echo calls made.\n\n        Returns:\n            The echo count\n        \"\"\"\n        return self.echo_count\n\n    def close(self):\n        \"\"\"Clean up the echo app.\"\"\"\n        self.closed = True\n        self.echo_count = 0\n"
  },
  {
    "path": "tests/t.py",
    "content": "# Copyright 2009 Paul J. Davis <paul.joseph.davis@gmail.com>\n#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport io\nimport os\nimport tempfile\n\ndirname = os.path.dirname(__file__)\n\nfrom gunicorn.http.parser import RequestParser\n\n\ndef data_source(fname):\n    buf = io.BytesIO()\n    with open(fname) as handle:\n        for line in handle:\n            line = line.rstrip(\"\\n\").replace(\"\\\\r\\\\n\", \"\\r\\n\")\n            buf.write(line.encode('latin1'))\n        return buf\n\n\nclass request:\n    def __init__(self, name):\n        self.fname = os.path.join(dirname, \"requests\", name)\n\n    def __call__(self, func):\n        def run():\n            src = data_source(self.fname)\n            func(src, RequestParser(src, None, None))\n        run.func_name = func.func_name\n        return run\n\n\nclass FakeSocket:\n\n    def __init__(self, data):\n        self.tmp = tempfile.TemporaryFile()\n        if data:\n            self.tmp.write(data.getvalue())\n            self.tmp.flush()\n            self.tmp.seek(0)\n\n    def fileno(self):\n        return self.tmp.fileno()\n\n    def len(self):\n        return self.tmp.len\n\n    def recv(self, length=None):\n        return self.tmp.read(length)\n\n    def recv_into(self, buf, length):\n        tmp_buffer = self.tmp.read(length)\n        v = len(tmp_buffer)\n        for i, c in enumerate(tmp_buffer):\n            buf[i] = c\n        return v\n\n    def send(self, data):\n        self.tmp.write(data)\n        self.tmp.flush()\n\n    def seek(self, offset, whence=0):\n        self.tmp.seek(offset, whence)\n"
  },
  {
    "path": "tests/test_arbiter.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport os\nimport signal\nfrom unittest import mock\n\nimport pytest\n\nimport gunicorn.app.base\nimport gunicorn.arbiter\nimport gunicorn.errors\nfrom gunicorn.config import ReusePort\n\n\nclass DummyApplication(gunicorn.app.base.BaseApplication):\n    \"\"\"\n    Dummy application that has a default configuration.\n    \"\"\"\n\n    def init(self, parser, opts, args):\n        \"\"\"No-op\"\"\"\n\n    def load(self):\n        \"\"\"No-op\"\"\"\n\n    def load_config(self):\n        \"\"\"No-op\"\"\"\n\n\n@mock.patch('gunicorn.sock.close_sockets')\ndef test_arbiter_stop_closes_listeners(close_sockets):\n    arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n    listener1 = mock.Mock()\n    listener2 = mock.Mock()\n    listeners = [listener1, listener2]\n    arbiter.LISTENERS = listeners\n    arbiter.stop()\n    close_sockets.assert_called_with(listeners, True)\n\n\n@mock.patch('gunicorn.sock.close_sockets')\ndef test_arbiter_stop_child_does_not_unlink_listeners(close_sockets):\n    arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n    arbiter.reexec_pid = os.getpid()\n    arbiter.stop()\n    close_sockets.assert_called_with([], False)\n\n\n@mock.patch('gunicorn.sock.close_sockets')\ndef test_arbiter_stop_parent_does_not_unlink_listeners(close_sockets):\n    arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n    arbiter.master_pid = os.getppid()\n    arbiter.stop()\n    close_sockets.assert_called_with([], False)\n\n\n@mock.patch('gunicorn.sock.close_sockets')\ndef test_arbiter_stop_does_not_unlink_systemd_listeners(close_sockets):\n    arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n    arbiter.systemd = True\n    arbiter.stop()\n    close_sockets.assert_called_with([], False)\n\n\n@mock.patch('gunicorn.sock.close_sockets')\ndef test_arbiter_stop_does_not_unlink_when_using_reuse_port(close_sockets):\n    arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n    arbiter.cfg.settings['reuse_port'] = ReusePort()\n    arbiter.cfg.settings['reuse_port'].set(True)\n    arbiter.stop()\n    close_sockets.assert_called_with([], False)\n\n\n@mock.patch('os.getpid')\n@mock.patch('os.fork')\n@mock.patch('os.execvpe')\ndef test_arbiter_reexec_passing_systemd_sockets(execvpe, fork, getpid):\n    arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n    arbiter.LISTENERS = [mock.Mock(), mock.Mock()]\n    arbiter.systemd = True\n    fork.return_value = 0\n    getpid.side_effect = [2, 3]\n    arbiter.reexec()\n    environ = execvpe.call_args[0][2]\n    assert environ['GUNICORN_PID'] == '2'\n    assert environ['LISTEN_FDS'] == '2'\n    assert environ['LISTEN_PID'] == '3'\n\n\n@mock.patch('os.getpid')\n@mock.patch('os.fork')\n@mock.patch('os.execvpe')\ndef test_arbiter_reexec_passing_gunicorn_sockets(execvpe, fork, getpid):\n    arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n    listener1 = mock.Mock()\n    listener2 = mock.Mock()\n    listener1.fileno.return_value = 4\n    listener2.fileno.return_value = 5\n    arbiter.LISTENERS = [listener1, listener2]\n    fork.return_value = 0\n    getpid.side_effect = [2, 3]\n    arbiter.reexec()\n    environ = execvpe.call_args[0][2]\n    assert environ['GUNICORN_FD'] == '4,5'\n    assert environ['GUNICORN_PID'] == '2'\n\n\n@mock.patch('os.fork')\ndef test_arbiter_reexec_limit_parent(fork):\n    arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n    arbiter.reexec_pid = ~os.getpid()\n    arbiter.reexec()\n    assert fork.called is False, \"should not fork when there is already a child\"\n\n\n@mock.patch('os.fork')\ndef test_arbiter_reexec_limit_child(fork):\n    arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n    arbiter.master_pid = ~os.getpid()\n    arbiter.reexec()\n    assert fork.called is False, \"should not fork when arbiter is a child\"\n\n\n@mock.patch('os.fork')\ndef test_arbiter_calls_worker_exit(mock_os_fork):\n    mock_os_fork.return_value = 0\n\n    arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n    arbiter.cfg.settings['worker_exit'] = mock.Mock()\n    arbiter.pid = None\n    mock_worker = mock.Mock()\n    arbiter.worker_class = mock.Mock(return_value=mock_worker)\n    try:\n        arbiter.spawn_worker()\n    except SystemExit:\n        pass\n    arbiter.cfg.worker_exit.assert_called_with(arbiter, mock_worker)\n\n\n@mock.patch('os.waitpid')\ndef test_arbiter_reap_workers(mock_os_waitpid):\n    mock_os_waitpid.side_effect = [(42, 0), (0, 0)]\n    arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n    arbiter.cfg.settings['child_exit'] = mock.Mock()\n    mock_worker = mock.Mock()\n    arbiter.WORKERS = {42: mock_worker}\n    arbiter.reap_workers()\n    mock_worker.tmp.close.assert_called_with()\n    arbiter.cfg.child_exit.assert_called_with(arbiter, mock_worker)\n\n\nclass PreloadedAppWithEnvSettings(DummyApplication):\n    \"\"\"\n    Simple application that makes use of the 'preload' feature to\n    start the application before spawning worker processes and sets\n    environmental variable configuration settings.\n    \"\"\"\n\n    def load_config(self):\n        \"\"\"Set the 'preload_app' and 'raw_env' settings in order to verify their\n        interaction below.\n        \"\"\"\n        self.cfg.set('raw_env', [\n            'SOME_PATH=/tmp/something', 'OTHER_PATH=/tmp/something/else'])\n        self.cfg.set('preload_app', True)\n\n    def wsgi(self):\n        \"\"\"Assert that the expected environmental variables are set when\n        the main entry point of this application is called as part of a\n        'preloaded' application.\n        \"\"\"\n        verify_env_vars()\n        return super().wsgi()\n\n\ndef verify_env_vars():\n    assert os.getenv('SOME_PATH') == '/tmp/something'\n    assert os.getenv('OTHER_PATH') == '/tmp/something/else'\n\n\ndef test_env_vars_available_during_preload():\n    \"\"\"Ensure that configured environmental variables are set during the\n    initial set up of the application (called from the .setup() method of\n    the Arbiter) such that they are available during the initial loading\n    of the WSGI application.\n    \"\"\"\n    # Note that we aren't making any assertions here, they are made in the\n    # dummy application object being loaded here instead.\n    gunicorn.arbiter.Arbiter(PreloadedAppWithEnvSettings())\n\n\n# ============================================================================\n# Signal Handler Registration Tests\n# ============================================================================\n\nclass TestSignalHandlerRegistration:\n    \"\"\"Tests for signal handler registration during arbiter initialization.\"\"\"\n\n    def test_init_signals_registers_all_signals(self):\n        \"\"\"Verify that init_signals registers handlers for all expected signals.\"\"\"\n        arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n\n        with mock.patch('signal.signal') as mock_signal:\n            arbiter.init_signals()\n\n            # Verify all expected signals are registered\n            registered_signals = {call[0][0] for call in mock_signal.call_args_list}\n            expected_signals = set(arbiter.SIGNALS)\n            expected_signals.add(signal.SIGCHLD)\n\n            assert expected_signals.issubset(registered_signals), \\\n                f\"Missing signals: {expected_signals - registered_signals}\"\n\n    def test_init_signals_creates_queue(self):\n        \"\"\"Verify that arbiter has a SimpleQueue for signals.\"\"\"\n        arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n\n        # Verify SimpleQueue was created\n        import queue\n        assert isinstance(arbiter.SIG_QUEUE, queue.SimpleQueue)\n\n    def test_sigchld_has_separate_handler(self):\n        \"\"\"Verify that SIGCHLD uses a separate signal handler from other signals.\"\"\"\n        arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n\n        with mock.patch('signal.signal') as mock_signal:\n            arbiter.init_signals()\n\n            # Find the handler for SIGCHLD - uses signal_chld for async-signal-safety\n            sigchld_calls = [c for c in mock_signal.call_args_list\n                            if c[0][0] == signal.SIGCHLD]\n            assert len(sigchld_calls) == 1\n            assert sigchld_calls[0][0][1] == arbiter.signal_chld\n\n            # Find handlers for other signals\n            other_calls = [c for c in mock_signal.call_args_list\n                          if c[0][0] in arbiter.SIGNALS]\n            for call in other_calls:\n                assert call[0][1] == arbiter.signal\n\n    def test_signals_list_contains_expected(self):\n        \"\"\"Verify that SIGNALS list contains all expected signal types.\"\"\"\n        arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n\n        expected = ['HUP', 'QUIT', 'INT', 'TERM', 'TTIN', 'TTOU',\n                    'USR1', 'USR2', 'WINCH']\n        for name in expected:\n            sig = getattr(signal, f'SIG{name}')\n            assert sig in arbiter.SIGNALS, f\"SIG{name} not in SIGNALS list\"\n\n\n# ============================================================================\n# Signal Queue Tests\n# ============================================================================\n\nclass TestSignalQueue:\n    \"\"\"Tests for signal queueing and wakeup mechanism using SimpleQueue.\"\"\"\n\n    def test_signal_queued_on_receipt(self):\n        \"\"\"Verify that signals are queued when the signal handler is called.\"\"\"\n        arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n\n        arbiter.signal(signal.SIGHUP, None)\n\n        # Get the signal from the queue\n        sig = arbiter.SIG_QUEUE.get_nowait()\n        assert sig == signal.SIGHUP\n\n    def test_multiple_signals_queued(self):\n        \"\"\"Verify that multiple signals can be queued.\"\"\"\n        arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n\n        # Queue multiple signals\n        arbiter.signal(signal.SIGHUP, None)\n        arbiter.signal(signal.SIGTERM, None)\n        arbiter.signal_chld(signal.SIGCHLD, None)\n\n        signals = []\n        while True:\n            try:\n                signals.append(arbiter.SIG_QUEUE.get_nowait())\n            except Exception:\n                break\n\n        assert signal.SIGHUP in signals\n        assert signal.SIGTERM in signals\n        assert signal.SIGCHLD in signals\n\n    def test_wakeup_puts_sentinel(self):\n        \"\"\"Verify that wakeup puts the WAKEUP_REQUEST sentinel to the queue.\"\"\"\n        arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n\n        arbiter.wakeup()\n\n        sig = arbiter.SIG_QUEUE.get_nowait()\n        assert sig == arbiter.WAKEUP_REQUEST\n\n    def test_wait_for_signals_returns_signals(self):\n        \"\"\"Verify that wait_for_signals returns queued signals.\"\"\"\n        arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n\n        # Queue some signals\n        arbiter.SIG_QUEUE.put_nowait(signal.SIGHUP)\n        arbiter.SIG_QUEUE.put_nowait(signal.SIGTERM)\n\n        signals = arbiter.wait_for_signals(timeout=0.1)\n\n        assert signal.SIGHUP in signals\n        assert signal.SIGTERM in signals\n\n    def test_wait_for_signals_filters_wakeup_request(self):\n        \"\"\"Verify that WAKEUP_REQUEST sentinel is filtered from results.\"\"\"\n        arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n\n        # Queue a wakeup request and a real signal\n        arbiter.SIG_QUEUE.put_nowait(arbiter.WAKEUP_REQUEST)\n        arbiter.SIG_QUEUE.put_nowait(signal.SIGHUP)\n\n        signals = arbiter.wait_for_signals(timeout=0.1)\n\n        assert arbiter.WAKEUP_REQUEST not in signals\n        assert signal.SIGHUP in signals\n\n\n# ============================================================================\n# Reap Workers Tests\n# ============================================================================\n\nclass TestReapWorkers:\n    \"\"\"Tests for worker reaping and exit status handling.\"\"\"\n\n    @mock.patch('os.waitpid')\n    def test_reap_normal_exit(self, mock_waitpid):\n        \"\"\"Verify that a worker with normal exit (code 0) is properly reaped.\"\"\"\n        mock_waitpid.side_effect = [(42, 0), (0, 0)]\n\n        arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n        arbiter.cfg.settings['child_exit'] = mock.Mock()\n        mock_worker = mock.Mock()\n        arbiter.WORKERS = {42: mock_worker}\n\n        arbiter.reap_workers()\n\n        mock_worker.tmp.close.assert_called_once()\n        arbiter.cfg.child_exit.assert_called_once_with(arbiter, mock_worker)\n        assert 42 not in arbiter.WORKERS\n\n    @mock.patch('os.waitpid')\n    def test_reap_exit_with_error_code(self, mock_waitpid):\n        \"\"\"Verify that a worker exiting with non-zero code is logged.\"\"\"\n        # Exit code 1 (status = 1 << 8 = 256)\n        mock_waitpid.side_effect = [(42, 256), (0, 0)]\n\n        arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n        arbiter.cfg.settings['child_exit'] = mock.Mock()\n        mock_worker = mock.Mock()\n        arbiter.WORKERS = {42: mock_worker}\n\n        with mock.patch.object(arbiter.log, 'error') as mock_log:\n            arbiter.reap_workers()\n\n        # Should log the error exit\n        assert any('exited with code' in str(call) for call in mock_log.call_args_list)\n\n    @mock.patch('os.waitpid')\n    def test_reap_worker_boot_error(self, mock_waitpid):\n        \"\"\"Verify that WORKER_BOOT_ERROR causes HaltServer.\"\"\"\n        # Exit code 3 (WORKER_BOOT_ERROR) = status 3 << 8 = 768\n        mock_waitpid.side_effect = [(42, 768), (0, 0)]\n\n        arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n        arbiter.cfg.settings['child_exit'] = mock.Mock()\n        mock_worker = mock.Mock()\n        arbiter.WORKERS = {42: mock_worker}\n\n        with pytest.raises(gunicorn.errors.HaltServer) as exc_info:\n            arbiter.reap_workers()\n\n        assert exc_info.value.exit_status == gunicorn.arbiter.Arbiter.WORKER_BOOT_ERROR\n\n    @mock.patch('os.waitpid')\n    def test_reap_app_load_error(self, mock_waitpid):\n        \"\"\"Verify that APP_LOAD_ERROR causes HaltServer.\"\"\"\n        # Exit code 4 (APP_LOAD_ERROR) = status 4 << 8 = 1024\n        mock_waitpid.side_effect = [(42, 1024), (0, 0)]\n\n        arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n        arbiter.cfg.settings['child_exit'] = mock.Mock()\n        mock_worker = mock.Mock()\n        arbiter.WORKERS = {42: mock_worker}\n\n        with pytest.raises(gunicorn.errors.HaltServer) as exc_info:\n            arbiter.reap_workers()\n\n        assert exc_info.value.exit_status == gunicorn.arbiter.Arbiter.APP_LOAD_ERROR\n\n    @mock.patch('os.waitpid')\n    def test_reap_killed_by_signal(self, mock_waitpid):\n        \"\"\"Verify that a worker killed by signal is properly identified.\"\"\"\n        # Status for SIGTERM (15) killed process\n        mock_waitpid.side_effect = [(42, signal.SIGTERM), (0, 0)]\n\n        arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n        arbiter.cfg.settings['child_exit'] = mock.Mock()\n        mock_worker = mock.Mock()\n        arbiter.WORKERS = {42: mock_worker}\n\n        # SIGTERM should be logged as info (expected during graceful shutdown)\n        with mock.patch.object(arbiter.log, 'info') as mock_log:\n            arbiter.reap_workers()\n\n        # Should log the signal\n        assert any('SIGTERM' in str(call) for call in mock_log.call_args_list)\n\n    @mock.patch('os.waitpid')\n    def test_reap_killed_by_sigkill_oom_hint(self, mock_waitpid):\n        \"\"\"Verify that SIGKILL adds OOM hint to log message.\"\"\"\n        # Status for SIGKILL (9) killed process\n        mock_waitpid.side_effect = [(42, signal.SIGKILL), (0, 0)]\n\n        arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n        arbiter.cfg.settings['child_exit'] = mock.Mock()\n        mock_worker = mock.Mock()\n        arbiter.WORKERS = {42: mock_worker}\n\n        with mock.patch.object(arbiter.log, 'error') as mock_log:\n            arbiter.reap_workers()\n\n        # Should include OOM hint\n        log_messages = ' '.join(str(call) for call in mock_log.call_args_list)\n        assert 'out of memory' in log_messages.lower()\n\n\n# ============================================================================\n# SIGHUP Reload Tests\n# ============================================================================\n\nclass TestSighupReload:\n    \"\"\"Tests for SIGHUP (reload) handling.\"\"\"\n\n    @mock.patch('gunicorn.arbiter.Arbiter.spawn_worker')\n    @mock.patch('gunicorn.arbiter.Arbiter.manage_workers')\n    def test_reload_spawns_new_workers(self, mock_manage, mock_spawn):\n        \"\"\"Verify that reload spawns the configured number of workers.\"\"\"\n        arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n        arbiter.cfg.set('workers', 3)\n        arbiter.LISTENERS = [mock.Mock()]\n        arbiter.pidfile = None\n        # Mock app.reload to prevent it from resetting config\n        arbiter.app.reload = mock.Mock()\n        # Mock setup to prevent it from resetting num_workers\n        arbiter.setup = mock.Mock()\n\n        arbiter.reload()\n\n        assert mock_spawn.call_count == 3\n\n    @mock.patch('gunicorn.arbiter.Arbiter.spawn_worker')\n    @mock.patch('gunicorn.arbiter.Arbiter.manage_workers')\n    def test_reload_calls_manage_workers(self, mock_manage, mock_spawn):\n        \"\"\"Verify that reload calls manage_workers after spawning.\"\"\"\n        arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n        arbiter.cfg.set('workers', 1)\n        arbiter.LISTENERS = [mock.Mock()]\n        arbiter.pidfile = None\n\n        arbiter.reload()\n\n        mock_manage.assert_called_once()\n\n    @mock.patch('gunicorn.arbiter.Arbiter.spawn_worker')\n    @mock.patch('gunicorn.arbiter.Arbiter.manage_workers')\n    def test_reload_logs_hang_up(self, mock_manage, mock_spawn):\n        \"\"\"Verify that handle_hup logs the hang up message.\"\"\"\n        arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n        arbiter.LISTENERS = [mock.Mock()]\n        arbiter.pidfile = None\n\n        with mock.patch.object(arbiter.log, 'info') as mock_log:\n            arbiter.handle_hup()\n\n        # Check that \"Hang up\" was logged\n        assert any('Hang up' in str(call) for call in mock_log.call_args_list)\n\n\n# ============================================================================\n# Worker Lifecycle Tests\n# ============================================================================\n\nclass TestWorkerLifecycle:\n    \"\"\"Tests for worker spawning, killing, and lifecycle management.\"\"\"\n\n    @mock.patch('os.fork')\n    def test_spawn_worker_adds_to_workers_dict(self, mock_fork):\n        \"\"\"Verify that spawn_worker adds the worker to WORKERS dict.\"\"\"\n        mock_fork.return_value = 12345  # Non-zero = parent process\n\n        arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n        arbiter.WORKERS = {}\n        arbiter.pid = os.getpid()\n        arbiter.LISTENERS = []\n\n        pid = arbiter.spawn_worker()\n\n        assert pid == 12345\n        assert 12345 in arbiter.WORKERS\n        assert arbiter.WORKERS[12345].age == arbiter.worker_age\n\n    def test_kill_worker_sends_signal(self):\n        \"\"\"Verify that kill_worker sends the specified signal.\"\"\"\n        arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n        mock_worker = mock.Mock()\n        arbiter.WORKERS = {42: mock_worker}\n\n        with mock.patch('os.kill') as mock_kill:\n            arbiter.kill_worker(42, signal.SIGTERM)\n\n        mock_kill.assert_called_once_with(42, signal.SIGTERM)\n\n    def test_murder_workers_sends_sigabrt_first(self):\n        \"\"\"Verify that murder_workers sends SIGABRT on first timeout.\"\"\"\n        arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n        arbiter.timeout = 30\n\n        mock_worker = mock.Mock()\n        mock_worker.aborted = False\n        # Simulate timeout by returning a very old update time\n        mock_worker.tmp.last_update.return_value = 0\n        arbiter.WORKERS = {42: mock_worker}\n\n        with mock.patch('time.monotonic', return_value=100), \\\n             mock.patch.object(arbiter, 'kill_worker') as mock_kill:\n            arbiter.murder_workers()\n\n        mock_kill.assert_called_once_with(42, signal.SIGABRT)\n        assert mock_worker.aborted is True\n\n    def test_murder_workers_sends_sigkill_second(self):\n        \"\"\"Verify that murder_workers sends SIGKILL on second timeout.\"\"\"\n        arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n        arbiter.timeout = 30\n\n        mock_worker = mock.Mock()\n        mock_worker.aborted = True  # Already aborted once\n        mock_worker.tmp.last_update.return_value = 0\n        arbiter.WORKERS = {42: mock_worker}\n\n        with mock.patch('time.monotonic', return_value=100), \\\n             mock.patch.object(arbiter, 'kill_worker') as mock_kill:\n            arbiter.murder_workers()\n\n        mock_kill.assert_called_once_with(42, signal.SIGKILL)\n\n\n# ============================================================================\n# Dirty Arbiter Orphan Cleanup Tests\n# ============================================================================\n\nclass TestDirtyArbiterOrphanCleanup:\n    \"\"\"Tests for dirty arbiter orphan detection and cleanup.\"\"\"\n\n    def test_get_dirty_pidfile_path(self):\n        \"\"\"Verify pidfile path is generated correctly.\"\"\"\n        arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n        arbiter.proc_name = 'myapp'\n\n        path = arbiter._get_dirty_pidfile_path()\n\n        import tempfile\n        expected = os.path.join(tempfile.gettempdir(), 'gunicorn-dirty-myapp.pid')\n        assert path == expected\n\n    def test_get_dirty_pidfile_path_sanitizes_name(self):\n        \"\"\"Verify special characters in proc_name are sanitized.\"\"\"\n        arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n        arbiter.proc_name = 'my/app name'\n\n        path = arbiter._get_dirty_pidfile_path()\n\n        import tempfile\n        expected = os.path.join(tempfile.gettempdir(), 'gunicorn-dirty-my_app_name.pid')\n        assert path == expected\n\n    def test_get_dirty_pidfile_path_uses_proc_name_not_cfg(self):\n        \"\"\"Verify pidfile path uses self.proc_name for USR2 compatibility.\n\n        During USR2, self.proc_name becomes 'myapp.2' while self.cfg.proc_name\n        stays 'myapp'. Using self.proc_name ensures new and old dirty arbiters\n        have different PID file paths.\n        \"\"\"\n        arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n        arbiter.cfg.set('proc_name', 'myapp')\n        arbiter.proc_name = 'myapp.2'  # Simulates USR2 child\n\n        path = arbiter._get_dirty_pidfile_path()\n\n        import tempfile\n        # Should use self.proc_name, not self.cfg.proc_name\n        expected = os.path.join(tempfile.gettempdir(), 'gunicorn-dirty-myapp.2.pid')\n        assert path == expected\n\n    def test_cleanup_orphaned_skipped_during_usr2(self):\n        \"\"\"Verify cleanup is skipped during USR2 upgrade (master_pid != 0).\"\"\"\n        arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n        arbiter.master_pid = 12345  # Indicates USR2 upgrade in progress\n\n        with mock.patch.object(arbiter, '_get_dirty_pidfile_path') as mock_path:\n            arbiter._cleanup_orphaned_dirty_arbiter()\n\n        # Should not even check the pidfile path\n        mock_path.assert_not_called()\n\n    def test_cleanup_orphaned_no_pidfile(self):\n        \"\"\"Verify cleanup handles missing pidfile gracefully.\"\"\"\n        arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n        arbiter.master_pid = 0\n\n        with mock.patch('os.path.exists', return_value=False):\n            # Should not raise any exception\n            arbiter._cleanup_orphaned_dirty_arbiter()\n\n    @mock.patch('os.unlink')\n    @mock.patch('os.kill')\n    def test_cleanup_orphaned_kills_existing_process(self, mock_kill, mock_unlink):\n        \"\"\"Verify cleanup kills orphaned dirty arbiter process.\"\"\"\n        arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n        arbiter.master_pid = 0\n\n        # First kill(pid, 0) succeeds (process exists), then SIGTERM causes exit\n        mock_kill.side_effect = [None, None, OSError(3, \"No such process\")]\n\n        import tempfile\n        pidfile = os.path.join(tempfile.gettempdir(), 'gunicorn-dirty-test.pid')\n\n        with mock.patch('os.path.exists', return_value=True), \\\n             mock.patch('builtins.open', mock.mock_open(read_data='12345')), \\\n             mock.patch.object(arbiter, '_get_dirty_pidfile_path', return_value=pidfile), \\\n             mock.patch('time.sleep'):\n            arbiter._cleanup_orphaned_dirty_arbiter()\n\n        # Should have sent signal 0 (check), then SIGTERM\n        assert mock_kill.call_args_list[0] == mock.call(12345, 0)\n        assert mock_kill.call_args_list[1] == mock.call(12345, signal.SIGTERM)\n        # Should unlink the stale pidfile\n        mock_unlink.assert_called_with(pidfile)\n\n    @mock.patch('os.unlink')\n    @mock.patch('os.kill')\n    def test_cleanup_orphaned_sigkill_if_sigterm_fails(self, mock_kill, mock_unlink):\n        \"\"\"Verify cleanup sends SIGKILL if SIGTERM doesn't work.\"\"\"\n        arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n        arbiter.master_pid = 0\n\n        # Process exists on all checks until SIGKILL\n        def kill_side_effect(pid, sig):\n            if sig == signal.SIGKILL:\n                return None\n            return None  # Process still running\n\n        mock_kill.side_effect = kill_side_effect\n\n        import tempfile\n        pidfile = os.path.join(tempfile.gettempdir(), 'gunicorn-dirty-test.pid')\n\n        with mock.patch('os.path.exists', return_value=True), \\\n             mock.patch('builtins.open', mock.mock_open(read_data='12345')), \\\n             mock.patch.object(arbiter, '_get_dirty_pidfile_path', return_value=pidfile), \\\n             mock.patch('time.sleep'):\n            arbiter._cleanup_orphaned_dirty_arbiter()\n\n        # Should end with SIGKILL\n        kill_calls = [c for c in mock_kill.call_args_list if c[0][1] == signal.SIGKILL]\n        assert len(kill_calls) == 1\n\n    @mock.patch('os.unlink')\n    def test_cleanup_orphaned_stale_pidfile_no_process(self, mock_unlink):\n        \"\"\"Verify cleanup removes stale pidfile when process doesn't exist.\"\"\"\n        arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n        arbiter.master_pid = 0\n\n        import tempfile\n        pidfile = os.path.join(tempfile.gettempdir(), 'gunicorn-dirty-test.pid')\n\n        with mock.patch('os.path.exists', return_value=True), \\\n             mock.patch('builtins.open', mock.mock_open(read_data='12345')), \\\n             mock.patch.object(arbiter, '_get_dirty_pidfile_path', return_value=pidfile), \\\n             mock.patch('os.kill', side_effect=OSError(3, \"No such process\")):\n            arbiter._cleanup_orphaned_dirty_arbiter()\n\n        # Should still unlink the stale pidfile\n        mock_unlink.assert_called_with(pidfile)\n\n    @mock.patch('gunicorn.dirty.DirtyArbiter')\n    @mock.patch('os.fork')\n    def test_spawn_dirty_arbiter_calls_cleanup(self, mock_fork, mock_dirty_arbiter):\n        \"\"\"Verify spawn_dirty_arbiter calls orphan cleanup before spawning.\"\"\"\n        mock_fork.return_value = 12345  # Parent process\n        mock_arbiter_instance = mock.Mock()\n        mock_arbiter_instance.socket_path = '/tmp/test.sock'\n        mock_dirty_arbiter.return_value = mock_arbiter_instance\n\n        arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n        arbiter.cfg.set('dirty_workers', 1)\n        arbiter.cfg.set('dirty_apps', ['test:app'])\n\n        with mock.patch.object(arbiter, '_cleanup_orphaned_dirty_arbiter') as mock_cleanup, \\\n             mock.patch.object(arbiter, '_get_dirty_pidfile_path', return_value='/tmp/test.pid'), \\\n             mock.patch('gunicorn.dirty.set_dirty_socket_path'):\n            arbiter.spawn_dirty_arbiter()\n\n        mock_cleanup.assert_called_once()\n\n    @mock.patch('os.fork')\n    def test_spawn_dirty_arbiter_passes_pidfile(self, mock_fork):\n        \"\"\"Verify spawn_dirty_arbiter passes pidfile to DirtyArbiter.\"\"\"\n        mock_fork.return_value = 12345  # Parent process\n\n        arbiter = gunicorn.arbiter.Arbiter(DummyApplication())\n        arbiter.cfg.set('dirty_workers', 1)\n        arbiter.cfg.set('dirty_apps', ['test:app'])\n\n        pidfile_path = '/tmp/gunicorn-dirty-test.pid'\n        # Note: DirtyArbiter is now lazily imported in spawn_dirty_arbiter(),\n        # so we mock it in gunicorn.dirty where it's defined\n        with mock.patch.object(arbiter, '_cleanup_orphaned_dirty_arbiter'), \\\n             mock.patch.object(arbiter, '_get_dirty_pidfile_path', return_value=pidfile_path), \\\n             mock.patch('gunicorn.dirty.DirtyArbiter') as mock_dirty_arbiter, \\\n             mock.patch('gunicorn.dirty.set_dirty_socket_path'):\n            mock_arbiter_instance = mock.Mock()\n            mock_arbiter_instance.socket_path = '/tmp/test.sock'\n            mock_dirty_arbiter.return_value = mock_arbiter_instance\n\n            arbiter.spawn_dirty_arbiter()\n\n            # Verify DirtyArbiter was called with pidfile parameter\n            mock_dirty_arbiter.assert_called_once()\n            call_kwargs = mock_dirty_arbiter.call_args[1]\n            assert call_kwargs.get('pidfile') == pidfile_path\n"
  },
  {
    "path": "tests/test_asgi.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nTests for ASGI worker components.\n\"\"\"\n\nimport asyncio\nimport io\nimport ipaddress\nimport pytest\nfrom unittest import mock\n\nfrom gunicorn.asgi.unreader import AsyncUnreader\nfrom gunicorn.asgi.message import AsyncRequest\n\n\nclass MockStreamReader:\n    \"\"\"Mock asyncio.StreamReader for testing.\"\"\"\n\n    def __init__(self, data):\n        self.data = data\n        self.pos = 0\n\n    async def read(self, size=-1):\n        if self.pos >= len(self.data):\n            return b\"\"\n        if size < 0:\n            result = self.data[self.pos:]\n            self.pos = len(self.data)\n        else:\n            result = self.data[self.pos:self.pos + size]\n            self.pos += size\n        return result\n\n    async def readexactly(self, n):\n        if self.pos + n > len(self.data):\n            raise asyncio.IncompleteReadError(\n                self.data[self.pos:], n\n            )\n        result = self.data[self.pos:self.pos + n]\n        self.pos += n\n        return result\n\n\nclass MockConfig:\n    \"\"\"Mock gunicorn config for testing.\"\"\"\n\n    def __init__(self):\n        self.is_ssl = False\n        self.proxy_protocol = \"off\"\n        self.proxy_allow_ips = [\"127.0.0.1\"]\n        self.forwarded_allow_ips = [\"127.0.0.1\"]\n        self._proxy_allow_networks = None\n        self._forwarded_allow_networks = None\n        self.secure_scheme_headers = {}\n        self.forwarder_headers = []\n        self.limit_request_line = 8190\n        self.limit_request_fields = 100\n        self.limit_request_field_size = 8190\n        self.permit_unconventional_http_method = False\n        self.permit_unconventional_http_version = False\n        self.permit_obsolete_folding = False\n        self.casefold_http_method = False\n        self.strip_header_spaces = False\n        self.header_map = \"refuse\"\n\n    def forwarded_allow_networks(self):\n        if self._forwarded_allow_networks is None:\n            self._forwarded_allow_networks = [\n                ipaddress.ip_network(addr)\n                for addr in self.forwarded_allow_ips\n                if addr != \"*\"\n            ]\n        return self._forwarded_allow_networks\n\n    def proxy_allow_networks(self):\n        if self._proxy_allow_networks is None:\n            self._proxy_allow_networks = [\n                ipaddress.ip_network(addr)\n                for addr in self.proxy_allow_ips\n                if addr != \"*\"\n            ]\n        return self._proxy_allow_networks\n\n\n# AsyncUnreader Tests\n\n@pytest.mark.asyncio\nasync def test_async_unreader_read_chunk():\n    \"\"\"Test basic chunk reading.\"\"\"\n    reader = MockStreamReader(b\"hello world\")\n    unreader = AsyncUnreader(reader)\n    data = await unreader.read()\n    assert data == b\"hello world\"\n\n\n@pytest.mark.asyncio\nasync def test_async_unreader_read_size():\n    \"\"\"Test reading specific size.\"\"\"\n    reader = MockStreamReader(b\"hello world\")\n    unreader = AsyncUnreader(reader)\n    data = await unreader.read(5)\n    assert data == b\"hello\"\n\n\n@pytest.mark.asyncio\nasync def test_async_unreader_unread():\n    \"\"\"Test unread functionality.\"\"\"\n    reader = MockStreamReader(b\"hello world\")\n    unreader = AsyncUnreader(reader)\n\n    # Read all data\n    data = await unreader.read()\n    assert data == b\"hello world\"\n\n    # Unread some data\n    unreader.unread(b\"world\")\n\n    # Read again should get unread data\n    data = await unreader.read()\n    assert data == b\"world\"\n\n\n@pytest.mark.asyncio\nasync def test_async_unreader_read_zero():\n    \"\"\"Test reading zero bytes.\"\"\"\n    reader = MockStreamReader(b\"hello\")\n    unreader = AsyncUnreader(reader)\n    data = await unreader.read(0)\n    assert data == b\"\"\n\n\n@pytest.mark.asyncio\nasync def test_async_unreader_read_empty():\n    \"\"\"Test reading from empty stream.\"\"\"\n    reader = MockStreamReader(b\"\")\n    unreader = AsyncUnreader(reader)\n    data = await unreader.read()\n    assert data == b\"\"\n\n\n# AsyncRequest Tests\n\n@pytest.mark.asyncio\nasync def test_async_request_simple_get():\n    \"\"\"Test parsing a simple GET request.\"\"\"\n    request_data = b\"GET /path HTTP/1.1\\r\\nHost: localhost\\r\\n\\r\\n\"\n    reader = MockStreamReader(request_data)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    request = await AsyncRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n\n    assert request.method == \"GET\"\n    assert request.path == \"/path\"\n    assert request.version == (1, 1)\n    assert (\"HOST\", \"localhost\") in request.headers\n\n\n@pytest.mark.asyncio\nasync def test_async_request_with_query():\n    \"\"\"Test parsing request with query string.\"\"\"\n    request_data = b\"GET /search?q=test&page=1 HTTP/1.1\\r\\nHost: localhost\\r\\n\\r\\n\"\n    reader = MockStreamReader(request_data)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    request = await AsyncRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n\n    assert request.method == \"GET\"\n    assert request.path == \"/search\"\n    assert request.query == \"q=test&page=1\"\n\n\n@pytest.mark.asyncio\nasync def test_async_request_post_with_body():\n    \"\"\"Test parsing POST request with body.\"\"\"\n    request_data = (\n        b\"POST /submit HTTP/1.1\\r\\n\"\n        b\"Host: localhost\\r\\n\"\n        b\"Content-Length: 11\\r\\n\"\n        b\"\\r\\n\"\n        b\"hello=world\"\n    )\n    reader = MockStreamReader(request_data)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    request = await AsyncRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n\n    assert request.method == \"POST\"\n    assert request.path == \"/submit\"\n    assert request.content_length == 11\n\n    # Read body\n    body = await request.read_body(100)\n    assert body == b\"hello=world\"\n\n\n@pytest.mark.asyncio\nasync def test_async_request_multiple_headers():\n    \"\"\"Test parsing request with multiple headers.\"\"\"\n    request_data = (\n        b\"GET / HTTP/1.1\\r\\n\"\n        b\"Host: localhost\\r\\n\"\n        b\"Accept: text/html\\r\\n\"\n        b\"Accept-Language: en-US\\r\\n\"\n        b\"Connection: keep-alive\\r\\n\"\n        b\"\\r\\n\"\n    )\n    reader = MockStreamReader(request_data)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    request = await AsyncRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n\n    assert len(request.headers) == 4\n    assert request.get_header(\"HOST\") == \"localhost\"\n    assert request.get_header(\"ACCEPT\") == \"text/html\"\n\n\n@pytest.mark.asyncio\nasync def test_async_request_should_close_http10():\n    \"\"\"Test connection close detection for HTTP/1.0.\"\"\"\n    request_data = b\"GET / HTTP/1.0\\r\\nHost: localhost\\r\\n\\r\\n\"\n    reader = MockStreamReader(request_data)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    request = await AsyncRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n\n    assert request.version == (1, 0)\n    assert request.should_close() is True\n\n\n@pytest.mark.asyncio\nasync def test_async_request_should_close_connection_header():\n    \"\"\"Test connection close detection with Connection header.\"\"\"\n    request_data = b\"GET / HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n\"\n    reader = MockStreamReader(request_data)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    request = await AsyncRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n\n    assert request.should_close() is True\n\n\n@pytest.mark.asyncio\nasync def test_async_request_keepalive():\n    \"\"\"Test keepalive detection.\"\"\"\n    request_data = b\"GET / HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: keep-alive\\r\\n\\r\\n\"\n    reader = MockStreamReader(request_data)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    request = await AsyncRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n\n    assert request.should_close() is False\n\n\n@pytest.mark.asyncio\nasync def test_async_request_no_body_for_get():\n    \"\"\"Test that GET requests have no body by default.\"\"\"\n    request_data = b\"GET / HTTP/1.1\\r\\nHost: localhost\\r\\n\\r\\n\"\n    reader = MockStreamReader(request_data)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    request = await AsyncRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n\n    assert request.content_length == 0\n    body = await request.read_body()\n    assert body == b\"\"\n\n\n# Error handling tests\n\n@pytest.mark.asyncio\nasync def test_async_request_invalid_method():\n    \"\"\"Test invalid HTTP method detection.\"\"\"\n    from gunicorn.http.errors import InvalidRequestMethod\n\n    request_data = b\"ge!t / HTTP/1.1\\r\\nHost: localhost\\r\\n\\r\\n\"\n    reader = MockStreamReader(request_data)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    with pytest.raises(InvalidRequestMethod):\n        await AsyncRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n\n\n@pytest.mark.asyncio\nasync def test_async_request_invalid_http_version():\n    \"\"\"Test invalid HTTP version detection.\"\"\"\n    from gunicorn.http.errors import InvalidHTTPVersion\n\n    request_data = b\"GET / HTTP/2.0\\r\\nHost: localhost\\r\\n\\r\\n\"\n    reader = MockStreamReader(request_data)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    with pytest.raises(InvalidHTTPVersion):\n        await AsyncRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n"
  },
  {
    "path": "tests/test_asgi_compliance.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nASGI 3.0 specification compliance tests.\n\nTests that gunicorn's ASGI implementation conforms to the ASGI 3.0 spec:\nhttps://asgi.readthedocs.io/en/latest/specs/main.html\n\"\"\"\n\nimport asyncio\nfrom unittest import mock\n\nfrom gunicorn.config import Config\n\n\n# ============================================================================\n# ASGI Version Tests\n# ============================================================================\n\nclass TestASGIVersion:\n    \"\"\"Test ASGI version information in scope.\"\"\"\n\n    def _create_protocol(self):\n        \"\"\"Create an ASGIProtocol instance for testing.\"\"\"\n        from gunicorn.asgi.protocol import ASGIProtocol\n\n        worker = mock.Mock()\n        worker.cfg = Config()\n        worker.log = mock.Mock()\n        worker.asgi = mock.Mock()\n\n        return ASGIProtocol(worker)\n\n    def _create_mock_request(self, **kwargs):\n        \"\"\"Create a mock HTTP request.\"\"\"\n        request = mock.Mock()\n        request.method = kwargs.get(\"method\", \"GET\")\n        request.path = kwargs.get(\"path\", \"/\")\n        request.query = kwargs.get(\"query\", \"\")\n        request.version = kwargs.get(\"version\", (1, 1))\n        request.scheme = kwargs.get(\"scheme\", \"http\")\n        request.headers = kwargs.get(\"headers\", [])\n        return request\n\n    def test_asgi_version_present(self):\n        \"\"\"Test that 'asgi' key is present in HTTP scope.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request()\n\n        scope = protocol._build_http_scope(\n            request,\n            (\"127.0.0.1\", 8000),\n            (\"127.0.0.1\", 12345),\n        )\n\n        assert \"asgi\" in scope\n\n    def test_asgi_version_is_dict(self):\n        \"\"\"Test that 'asgi' value is a dictionary.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request()\n\n        scope = protocol._build_http_scope(\n            request,\n            (\"127.0.0.1\", 8000),\n            (\"127.0.0.1\", 12345),\n        )\n\n        assert isinstance(scope[\"asgi\"], dict)\n\n    def test_asgi_version_value(self):\n        \"\"\"Test that ASGI version is '3.0'.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request()\n\n        scope = protocol._build_http_scope(\n            request,\n            (\"127.0.0.1\", 8000),\n            (\"127.0.0.1\", 12345),\n        )\n\n        assert scope[\"asgi\"][\"version\"] == \"3.0\"\n\n    def test_asgi_spec_version_present(self):\n        \"\"\"Test that spec_version is present in ASGI dict.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request()\n\n        scope = protocol._build_http_scope(\n            request,\n            (\"127.0.0.1\", 8000),\n            (\"127.0.0.1\", 12345),\n        )\n\n        assert \"spec_version\" in scope[\"asgi\"]\n\n    def test_asgi_spec_version_value(self):\n        \"\"\"Test that spec_version follows semantic versioning.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request()\n\n        scope = protocol._build_http_scope(\n            request,\n            (\"127.0.0.1\", 8000),\n            (\"127.0.0.1\", 12345),\n        )\n\n        spec_version = scope[\"asgi\"][\"spec_version\"]\n        # Should be in format \"X.Y\" (major.minor)\n        parts = spec_version.split(\".\")\n        assert len(parts) == 2\n        assert all(part.isdigit() for part in parts)\n\n\n# ============================================================================\n# HTTP Scope Keys Tests (ASGI HTTP Connection Scope)\n# ============================================================================\n\nclass TestHTTPScopeKeys:\n    \"\"\"Test required keys in HTTP connection scope per ASGI spec.\"\"\"\n\n    def _create_protocol(self):\n        \"\"\"Create an ASGIProtocol instance for testing.\"\"\"\n        from gunicorn.asgi.protocol import ASGIProtocol\n\n        worker = mock.Mock()\n        worker.cfg = Config()\n        worker.log = mock.Mock()\n        worker.asgi = mock.Mock()\n\n        return ASGIProtocol(worker)\n\n    def _create_mock_request(self, **kwargs):\n        \"\"\"Create a mock HTTP request.\"\"\"\n        request = mock.Mock()\n        request.method = kwargs.get(\"method\", \"GET\")\n        request.path = kwargs.get(\"path\", \"/\")\n        request.query = kwargs.get(\"query\", \"\")\n        request.version = kwargs.get(\"version\", (1, 1))\n        request.scheme = kwargs.get(\"scheme\", \"http\")\n        request.headers = kwargs.get(\"headers\", [])\n        return request\n\n    def test_type_key_present(self):\n        \"\"\"Test 'type' key is present and equals 'http'.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request()\n\n        scope = protocol._build_http_scope(\n            request,\n            (\"127.0.0.1\", 8000),\n            (\"127.0.0.1\", 12345),\n        )\n\n        assert scope[\"type\"] == \"http\"\n\n    def test_http_version_key_present(self):\n        \"\"\"Test 'http_version' key is present.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request()\n\n        scope = protocol._build_http_scope(\n            request,\n            (\"127.0.0.1\", 8000),\n            (\"127.0.0.1\", 12345),\n        )\n\n        assert \"http_version\" in scope\n        assert scope[\"http_version\"] == \"1.1\"\n\n    def test_http_version_formats(self):\n        \"\"\"Test various HTTP version formats.\"\"\"\n        protocol = self._create_protocol()\n\n        # HTTP/1.0\n        request_10 = self._create_mock_request(version=(1, 0))\n        scope_10 = protocol._build_http_scope(request_10, None, None)\n        assert scope_10[\"http_version\"] == \"1.0\"\n\n        # HTTP/1.1\n        request_11 = self._create_mock_request(version=(1, 1))\n        scope_11 = protocol._build_http_scope(request_11, None, None)\n        assert scope_11[\"http_version\"] == \"1.1\"\n\n    def test_method_key_present(self):\n        \"\"\"Test 'method' key is present and is uppercase string.\"\"\"\n        protocol = self._create_protocol()\n\n        for method in [\"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\", \"HEAD\", \"OPTIONS\"]:\n            request = self._create_mock_request(method=method)\n            scope = protocol._build_http_scope(request, None, None)\n            assert scope[\"method\"] == method\n            assert scope[\"method\"].isupper()\n\n    def test_scheme_key_present(self):\n        \"\"\"Test 'scheme' key is present.\"\"\"\n        protocol = self._create_protocol()\n\n        # HTTP\n        request_http = self._create_mock_request(scheme=\"http\")\n        scope_http = protocol._build_http_scope(request_http, None, None)\n        assert scope_http[\"scheme\"] == \"http\"\n\n        # HTTPS\n        request_https = self._create_mock_request(scheme=\"https\")\n        scope_https = protocol._build_http_scope(request_https, None, None)\n        assert scope_https[\"scheme\"] == \"https\"\n\n    def test_path_key_present(self):\n        \"\"\"Test 'path' key is present and starts with /.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(path=\"/api/users\")\n\n        scope = protocol._build_http_scope(request, None, None)\n\n        assert \"path\" in scope\n        assert scope[\"path\"] == \"/api/users\"\n        assert scope[\"path\"].startswith(\"/\")\n\n    def test_raw_path_key_present(self):\n        \"\"\"Test 'raw_path' key is present and is bytes.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(path=\"/api/users\")\n\n        scope = protocol._build_http_scope(request, None, None)\n\n        assert \"raw_path\" in scope\n        assert isinstance(scope[\"raw_path\"], bytes)\n        assert scope[\"raw_path\"] == b\"/api/users\"\n\n    def test_query_string_key_present(self):\n        \"\"\"Test 'query_string' key is present and is bytes.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(query=\"page=1&limit=10\")\n\n        scope = protocol._build_http_scope(request, None, None)\n\n        assert \"query_string\" in scope\n        assert isinstance(scope[\"query_string\"], bytes)\n        assert scope[\"query_string\"] == b\"page=1&limit=10\"\n\n    def test_query_string_empty(self):\n        \"\"\"Test 'query_string' is empty bytes when no query.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(query=\"\")\n\n        scope = protocol._build_http_scope(request, None, None)\n\n        assert scope[\"query_string\"] == b\"\"\n\n    def test_root_path_key_present(self):\n        \"\"\"Test 'root_path' key is present.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request()\n\n        scope = protocol._build_http_scope(request, None, None)\n\n        assert \"root_path\" in scope\n        assert isinstance(scope[\"root_path\"], str)\n\n    def test_headers_key_present(self):\n        \"\"\"Test 'headers' key is present and is list of 2-tuples.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(\n            headers=[(\"HOST\", \"localhost\"), (\"ACCEPT\", \"text/html\")]\n        )\n\n        scope = protocol._build_http_scope(request, None, None)\n\n        assert \"headers\" in scope\n        assert isinstance(scope[\"headers\"], list)\n\n        for header in scope[\"headers\"]:\n            assert isinstance(header, tuple)\n            assert len(header) == 2\n\n    def test_headers_are_bytes(self):\n        \"\"\"Test that header names and values are bytes.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(\n            headers=[(\"HOST\", \"localhost\"), (\"CONTENT-TYPE\", \"application/json\")]\n        )\n\n        scope = protocol._build_http_scope(request, None, None)\n\n        for name, value in scope[\"headers\"]:\n            assert isinstance(name, bytes), f\"Header name should be bytes: {name}\"\n            assert isinstance(value, bytes), f\"Header value should be bytes: {value}\"\n\n    def test_headers_names_lowercase(self):\n        \"\"\"Test that header names are lowercase.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(\n            headers=[(\"HOST\", \"localhost\"), (\"Content-Type\", \"application/json\")]\n        )\n\n        scope = protocol._build_http_scope(request, None, None)\n\n        for name, _ in scope[\"headers\"]:\n            assert name == name.lower(), f\"Header name should be lowercase: {name}\"\n\n    def test_server_key_present(self):\n        \"\"\"Test 'server' key is present when sockname provided.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request()\n\n        scope = protocol._build_http_scope(\n            request,\n            (\"127.0.0.1\", 8000),\n            (\"127.0.0.1\", 12345),\n        )\n\n        assert \"server\" in scope\n        assert scope[\"server\"] == (\"127.0.0.1\", 8000)\n\n    def test_server_key_none(self):\n        \"\"\"Test 'server' key is None when sockname not provided.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request()\n\n        scope = protocol._build_http_scope(request, None, None)\n\n        assert scope[\"server\"] is None\n\n    def test_client_key_present(self):\n        \"\"\"Test 'client' key is present when peername provided.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request()\n\n        scope = protocol._build_http_scope(\n            request,\n            (\"127.0.0.1\", 8000),\n            (\"192.168.1.100\", 54321),\n        )\n\n        assert \"client\" in scope\n        assert scope[\"client\"] == (\"192.168.1.100\", 54321)\n\n    def test_client_key_none(self):\n        \"\"\"Test 'client' key is None when peername not provided.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request()\n\n        scope = protocol._build_http_scope(request, None, None)\n\n        assert scope[\"client\"] is None\n\n\n# ============================================================================\n# HTTP Message Format Tests\n# ============================================================================\n\nclass TestHTTPMessageFormats:\n    \"\"\"Test HTTP message formats per ASGI spec.\"\"\"\n\n    def test_http_request_message_format(self):\n        \"\"\"Test http.request message format.\"\"\"\n        message = {\n            \"type\": \"http.request\",\n            \"body\": b\"request body\",\n            \"more_body\": False,\n        }\n\n        assert message[\"type\"] == \"http.request\"\n        assert isinstance(message[\"body\"], bytes)\n        assert isinstance(message[\"more_body\"], bool)\n\n    def test_http_request_message_empty_body(self):\n        \"\"\"Test http.request message with empty body.\"\"\"\n        message = {\n            \"type\": \"http.request\",\n            \"body\": b\"\",\n            \"more_body\": False,\n        }\n\n        assert message[\"body\"] == b\"\"\n        assert message[\"more_body\"] is False\n\n    def test_http_response_start_format(self):\n        \"\"\"Test http.response.start message format.\"\"\"\n        message = {\n            \"type\": \"http.response.start\",\n            \"status\": 200,\n            \"headers\": [\n                (b\"content-type\", b\"text/plain\"),\n                (b\"content-length\", b\"13\"),\n            ],\n        }\n\n        assert message[\"type\"] == \"http.response.start\"\n        assert isinstance(message[\"status\"], int)\n        assert 100 <= message[\"status\"] < 600\n        assert isinstance(message[\"headers\"], list)\n\n    def test_http_response_body_format(self):\n        \"\"\"Test http.response.body message format.\"\"\"\n        message = {\n            \"type\": \"http.response.body\",\n            \"body\": b\"Hello, World!\",\n            \"more_body\": False,\n        }\n\n        assert message[\"type\"] == \"http.response.body\"\n        assert isinstance(message[\"body\"], bytes)\n        assert isinstance(message[\"more_body\"], bool)\n\n    def test_http_response_body_streaming(self):\n        \"\"\"Test http.response.body message for streaming.\"\"\"\n        # First chunk\n        chunk1 = {\n            \"type\": \"http.response.body\",\n            \"body\": b\"First chunk\",\n            \"more_body\": True,\n        }\n\n        # Last chunk\n        chunk2 = {\n            \"type\": \"http.response.body\",\n            \"body\": b\"Last chunk\",\n            \"more_body\": False,\n        }\n\n        assert chunk1[\"more_body\"] is True\n        assert chunk2[\"more_body\"] is False\n\n    def test_http_disconnect_format(self):\n        \"\"\"Test http.disconnect message format.\"\"\"\n        message = {\"type\": \"http.disconnect\"}\n\n        assert message[\"type\"] == \"http.disconnect\"\n\n\n# ============================================================================\n# HTTP Response Status Codes Tests\n# ============================================================================\n\nclass TestHTTPStatusCodes:\n    \"\"\"Test HTTP status code handling.\"\"\"\n\n    def _create_protocol(self):\n        \"\"\"Create an ASGIProtocol instance for testing.\"\"\"\n        from gunicorn.asgi.protocol import ASGIProtocol\n\n        worker = mock.Mock()\n        worker.cfg = Config()\n        worker.log = mock.Mock()\n        worker.asgi = mock.Mock()\n\n        return ASGIProtocol(worker)\n\n    def test_reason_phrase_informational(self):\n        \"\"\"Test reason phrases for 1xx status codes.\"\"\"\n        protocol = self._create_protocol()\n\n        assert protocol._get_reason_phrase(100) == \"Continue\"\n        assert protocol._get_reason_phrase(101) == \"Switching Protocols\"\n        assert protocol._get_reason_phrase(103) == \"Early Hints\"\n\n    def test_reason_phrase_success(self):\n        \"\"\"Test reason phrases for 2xx status codes.\"\"\"\n        protocol = self._create_protocol()\n\n        assert protocol._get_reason_phrase(200) == \"OK\"\n        assert protocol._get_reason_phrase(201) == \"Created\"\n        assert protocol._get_reason_phrase(202) == \"Accepted\"\n        assert protocol._get_reason_phrase(204) == \"No Content\"\n        assert protocol._get_reason_phrase(206) == \"Partial Content\"\n\n    def test_reason_phrase_redirect(self):\n        \"\"\"Test reason phrases for 3xx status codes.\"\"\"\n        protocol = self._create_protocol()\n\n        assert protocol._get_reason_phrase(301) == \"Moved Permanently\"\n        assert protocol._get_reason_phrase(302) == \"Found\"\n        assert protocol._get_reason_phrase(303) == \"See Other\"\n        assert protocol._get_reason_phrase(304) == \"Not Modified\"\n        assert protocol._get_reason_phrase(307) == \"Temporary Redirect\"\n        assert protocol._get_reason_phrase(308) == \"Permanent Redirect\"\n\n    def test_reason_phrase_client_error(self):\n        \"\"\"Test reason phrases for 4xx status codes.\"\"\"\n        protocol = self._create_protocol()\n\n        assert protocol._get_reason_phrase(400) == \"Bad Request\"\n        assert protocol._get_reason_phrase(401) == \"Unauthorized\"\n        assert protocol._get_reason_phrase(403) == \"Forbidden\"\n        assert protocol._get_reason_phrase(404) == \"Not Found\"\n        assert protocol._get_reason_phrase(405) == \"Method Not Allowed\"\n        assert protocol._get_reason_phrase(408) == \"Request Timeout\"\n        assert protocol._get_reason_phrase(409) == \"Conflict\"\n        assert protocol._get_reason_phrase(410) == \"Gone\"\n        assert protocol._get_reason_phrase(422) == \"Unprocessable Entity\"\n        assert protocol._get_reason_phrase(429) == \"Too Many Requests\"\n\n    def test_reason_phrase_server_error(self):\n        \"\"\"Test reason phrases for 5xx status codes.\"\"\"\n        protocol = self._create_protocol()\n\n        assert protocol._get_reason_phrase(500) == \"Internal Server Error\"\n        assert protocol._get_reason_phrase(501) == \"Not Implemented\"\n        assert protocol._get_reason_phrase(502) == \"Bad Gateway\"\n        assert protocol._get_reason_phrase(503) == \"Service Unavailable\"\n        assert protocol._get_reason_phrase(504) == \"Gateway Timeout\"\n\n    def test_reason_phrase_unknown(self):\n        \"\"\"Test reason phrase for unknown status codes.\"\"\"\n        protocol = self._create_protocol()\n\n        assert protocol._get_reason_phrase(999) == \"Unknown\"\n        assert protocol._get_reason_phrase(418) == \"Unknown\"  # I'm a teapot not defined\n\n\n# ============================================================================\n# Informational Response Tests (103 Early Hints, etc.)\n# ============================================================================\n\nclass TestInformationalResponses:\n    \"\"\"Test support for HTTP 1xx informational responses.\"\"\"\n\n    def test_http_response_informational_format(self):\n        \"\"\"Test http.response.informational message format.\"\"\"\n        message = {\n            \"type\": \"http.response.informational\",\n            \"status\": 103,\n            \"headers\": [\n                (b\"link\", b\"</style.css>; rel=preload; as=style\"),\n            ],\n        }\n\n        assert message[\"type\"] == \"http.response.informational\"\n        assert 100 <= message[\"status\"] < 200\n        assert isinstance(message[\"headers\"], list)\n\n    def test_early_hints_103(self):\n        \"\"\"Test 103 Early Hints message format.\"\"\"\n        message = {\n            \"type\": \"http.response.informational\",\n            \"status\": 103,\n            \"headers\": [\n                (b\"link\", b\"</style.css>; rel=preload; as=style\"),\n                (b\"link\", b\"</script.js>; rel=preload; as=script\"),\n            ],\n        }\n\n        assert message[\"status\"] == 103\n\n\n# ============================================================================\n# ASGI Extensions Tests\n# ============================================================================\n\nclass TestASGIExtensions:\n    \"\"\"Test ASGI extensions support.\"\"\"\n\n    def _create_protocol(self):\n        \"\"\"Create an ASGIProtocol instance for testing.\"\"\"\n        from gunicorn.asgi.protocol import ASGIProtocol\n\n        worker = mock.Mock()\n        worker.cfg = Config()\n        worker.log = mock.Mock()\n        worker.asgi = mock.Mock()\n\n        return ASGIProtocol(worker)\n\n    def _create_mock_http2_request(self, **kwargs):\n        \"\"\"Create a mock HTTP/2 request with priority.\"\"\"\n        request = mock.Mock()\n        request.method = kwargs.get(\"method\", \"GET\")\n        request.path = kwargs.get(\"path\", \"/\")\n        request.query = kwargs.get(\"query\", \"\")\n        request.uri = kwargs.get(\"uri\", \"/\")\n        request.scheme = kwargs.get(\"scheme\", \"https\")\n        request.headers = kwargs.get(\"headers\", [])\n        request.priority_weight = kwargs.get(\"priority_weight\", 16)\n        request.priority_depends_on = kwargs.get(\"priority_depends_on\", 0)\n        return request\n\n    def test_http2_scope_has_extensions(self):\n        \"\"\"Test that HTTP/2 scope includes extensions dict.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_http2_request()\n\n        scope = protocol._build_http2_scope(request, None, None)\n\n        assert \"extensions\" in scope\n        assert isinstance(scope[\"extensions\"], dict)\n\n    def test_http2_priority_extension(self):\n        \"\"\"Test http.response.priority extension in HTTP/2 scope.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_http2_request(\n            priority_weight=128,\n            priority_depends_on=5,\n        )\n\n        scope = protocol._build_http2_scope(request, None, None)\n\n        assert \"http.response.priority\" in scope[\"extensions\"]\n        priority = scope[\"extensions\"][\"http.response.priority\"]\n        assert \"weight\" in priority\n        assert \"depends_on\" in priority\n        assert priority[\"weight\"] == 128\n        assert priority[\"depends_on\"] == 5\n\n    def test_http2_trailers_extension(self):\n        \"\"\"Test http.response.trailers extension in HTTP/2 scope.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_http2_request()\n\n        scope = protocol._build_http2_scope(request, None, None)\n\n        assert \"http.response.trailers\" in scope[\"extensions\"]\n\n    def test_http_response_trailers_message_format(self):\n        \"\"\"Test http.response.trailers message format.\"\"\"\n        message = {\n            \"type\": \"http.response.trailers\",\n            \"headers\": [\n                (b\"grpc-status\", b\"0\"),\n                (b\"grpc-message\", b\"\"),\n            ],\n            \"more_trailers\": False,\n        }\n\n        assert message[\"type\"] == \"http.response.trailers\"\n        assert isinstance(message[\"headers\"], list)\n\n\n# ============================================================================\n# State Sharing Tests\n# ============================================================================\n\nclass TestStateSharing:\n    \"\"\"Test state sharing between lifespan and request scopes.\"\"\"\n\n    def _create_protocol_with_state(self, state):\n        \"\"\"Create an ASGIProtocol with worker state.\"\"\"\n        from gunicorn.asgi.protocol import ASGIProtocol\n\n        worker = mock.Mock()\n        worker.cfg = Config()\n        worker.log = mock.Mock()\n        worker.asgi = mock.Mock()\n        worker.state = state\n\n        return ASGIProtocol(worker)\n\n    def _create_mock_request(self):\n        \"\"\"Create a mock HTTP request.\"\"\"\n        request = mock.Mock()\n        request.method = \"GET\"\n        request.path = \"/\"\n        request.query = \"\"\n        request.version = (1, 1)\n        request.scheme = \"http\"\n        request.headers = []\n        return request\n\n    def test_state_in_http_scope(self):\n        \"\"\"Test that state dict is included in HTTP scope.\"\"\"\n        state = {\"db\": \"connected\", \"cache\": \"ready\"}\n        protocol = self._create_protocol_with_state(state)\n        request = self._create_mock_request()\n\n        scope = protocol._build_http_scope(request, None, None)\n\n        assert \"state\" in scope\n        assert scope[\"state\"] == state\n\n    def test_state_is_same_object(self):\n        \"\"\"Test that state is the same object (not a copy).\"\"\"\n        state = {\"counter\": 0}\n        protocol = self._create_protocol_with_state(state)\n        request = self._create_mock_request()\n\n        scope = protocol._build_http_scope(request, None, None)\n\n        # Modifying scope[\"state\"] should modify the original\n        scope[\"state\"][\"counter\"] = 1\n        assert state[\"counter\"] == 1\n\n    def test_state_not_present_without_worker_state(self):\n        \"\"\"Test that state is not in scope if worker has no state.\"\"\"\n        from gunicorn.asgi.protocol import ASGIProtocol\n\n        worker = mock.Mock(spec=[\"cfg\", \"log\", \"asgi\"])\n        worker.cfg = Config()\n        worker.log = mock.Mock()\n        worker.asgi = mock.Mock()\n\n        protocol = ASGIProtocol(worker)\n        request = self._create_mock_request()\n\n        scope = protocol._build_http_scope(request, None, None)\n\n        assert \"state\" not in scope\n\n\n# ============================================================================\n# HTTP Disconnect Event Tests (ASGI Spec Compliance)\n# https://asgi.readthedocs.io/en/latest/specs/www.html#disconnect-receive-event\n# ============================================================================\n\nclass TestHTTPDisconnectEvent:\n    \"\"\"Test http.disconnect event compliance with ASGI spec.\n\n    Per the ASGI HTTP Connection Scope spec:\n    - Disconnect event is sent when client closes connection\n    - Event type MUST be \"http.disconnect\"\n    - Apps should receive this event and clean up gracefully\n    \"\"\"\n\n    def _create_protocol(self):\n        \"\"\"Create an ASGIProtocol instance for testing.\"\"\"\n        from gunicorn.asgi.protocol import ASGIProtocol\n\n        worker = mock.Mock()\n        worker.cfg = Config()\n        worker.log = mock.Mock()\n        worker.asgi = mock.Mock()\n        worker.nr_conns = 1\n        worker.loop = mock.Mock()\n\n        protocol = ASGIProtocol(worker)\n        protocol.reader = mock.Mock()\n\n        return protocol\n\n    def test_disconnect_event_type(self):\n        \"\"\"Test that disconnect event has correct type per ASGI spec.\"\"\"\n        protocol = self._create_protocol()\n        protocol._receive_queue = asyncio.Queue()\n\n        # Simulate client disconnect\n        protocol.connection_lost(None)\n\n        # Get the message from queue\n        msg = protocol._receive_queue.get_nowait()\n\n        # Per ASGI spec: type MUST be \"http.disconnect\"\n        assert msg[\"type\"] == \"http.disconnect\"\n\n    def test_disconnect_event_sent_on_connection_lost(self):\n        \"\"\"Test that http.disconnect is sent when connection is lost.\"\"\"\n        protocol = self._create_protocol()\n        protocol._receive_queue = asyncio.Queue()\n\n        assert protocol._receive_queue.empty()\n\n        # Simulate client disconnect\n        protocol.connection_lost(None)\n\n        # Queue should have disconnect message\n        assert not protocol._receive_queue.empty()\n\n    def test_disconnect_sets_closed_flag(self):\n        \"\"\"Test that connection_lost sets the closed flag.\"\"\"\n        protocol = self._create_protocol()\n\n        assert protocol._closed is False\n\n        protocol.connection_lost(None)\n\n        assert protocol._closed is True\n\n    def test_disconnect_allows_graceful_cleanup(self):\n        \"\"\"Test that disconnect doesn't immediately cancel task.\n\n        Per ASGI spec, apps should have opportunity to clean up\n        when they receive http.disconnect.\n        \"\"\"\n        protocol = self._create_protocol()\n\n        # Create a mock task\n        mock_task = mock.Mock()\n        mock_task.done.return_value = False\n        protocol._task = mock_task\n\n        # Simulate disconnect\n        protocol.connection_lost(None)\n\n        # Task should NOT be cancelled immediately\n        mock_task.cancel.assert_not_called()\n\n        # Cancellation should be scheduled after grace period\n        protocol.worker.loop.call_later.assert_called_once()\n\n    def test_disconnect_message_format(self):\n        \"\"\"Test http.disconnect message format per ASGI spec.\n\n        The disconnect message should only contain 'type' key.\n        \"\"\"\n        protocol = self._create_protocol()\n        protocol._receive_queue = asyncio.Queue()\n\n        protocol.connection_lost(None)\n\n        msg = protocol._receive_queue.get_nowait()\n\n        # Per ASGI spec, disconnect message only has 'type'\n        assert msg == {\"type\": \"http.disconnect\"}\n        assert len(msg) == 1\n"
  },
  {
    "path": "tests/test_asgi_disconnect.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nTests for ASGI graceful disconnect handling.\n\nIssue: https://github.com/benoitc/gunicorn/issues/3484\n\nWhen a client disconnects, the ASGI worker should:\n1. Send http.disconnect to the receive queue\n2. Allow the app a grace period to clean up\n3. Only cancel the task after the grace period\n\"\"\"\n\nimport asyncio\nfrom unittest import mock\n\nimport pytest\n\nfrom gunicorn.asgi.protocol import ASGIProtocol\n\n\nclass TestASGIGracefulDisconnect:\n    \"\"\"Test graceful disconnect handling.\"\"\"\n\n    @pytest.fixture\n    def mock_worker(self):\n        \"\"\"Create a mock worker.\"\"\"\n        worker = mock.Mock()\n        worker.nr_conns = 0\n        worker.loop = asyncio.new_event_loop()\n        worker.cfg = mock.Mock()\n        worker.cfg.asgi_disconnect_grace_period = 3\n        worker.log = mock.Mock()\n        return worker\n\n    def test_disconnect_sets_closed_flag(self, mock_worker):\n        \"\"\"Test that connection_lost sets the closed flag.\"\"\"\n        protocol = ASGIProtocol(mock_worker)\n        protocol.reader = mock.Mock()\n\n        # Simulate connection made\n        mock_worker.nr_conns = 1\n\n        assert protocol._closed is False\n\n        # Simulate connection lost\n        protocol.connection_lost(None)\n\n        assert protocol._closed is True\n\n    def test_disconnect_sends_message_to_queue(self, mock_worker):\n        \"\"\"Test that connection_lost sends http.disconnect to receive queue.\"\"\"\n        protocol = ASGIProtocol(mock_worker)\n        protocol.reader = mock.Mock()\n        mock_worker.nr_conns = 1\n\n        # Create a receive queue (simulating active request)\n        protocol._receive_queue = asyncio.Queue()\n\n        # Simulate connection lost\n        protocol.connection_lost(None)\n\n        # Check that disconnect message was sent\n        assert not protocol._receive_queue.empty()\n        msg = protocol._receive_queue.get_nowait()\n        assert msg == {\"type\": \"http.disconnect\"}\n\n    def test_disconnect_is_idempotent(self, mock_worker):\n        \"\"\"Test that connection_lost can be called multiple times safely.\"\"\"\n        protocol = ASGIProtocol(mock_worker)\n        protocol.reader = mock.Mock()\n        mock_worker.nr_conns = 2  # Start with 2 so we can verify only 1 is decremented\n\n        protocol._receive_queue = asyncio.Queue()\n\n        # First call should work\n        protocol.connection_lost(None)\n        assert protocol._closed is True\n        assert mock_worker.nr_conns == 1\n        assert protocol._receive_queue.qsize() == 1\n\n        # Second call should be a no-op\n        protocol.connection_lost(None)\n        assert mock_worker.nr_conns == 1  # Should not decrement again\n        assert protocol._receive_queue.qsize() == 1  # Should not add another message\n\n    def test_disconnect_does_not_cancel_immediately(self, mock_worker):\n        \"\"\"Test that connection_lost doesn't cancel task immediately.\"\"\"\n        protocol = ASGIProtocol(mock_worker)\n        protocol.reader = mock.Mock()\n        mock_worker.nr_conns = 1\n\n        # Create a mock task\n        mock_task = mock.Mock()\n        mock_task.done.return_value = False\n        protocol._task = mock_task\n\n        # Simulate connection lost\n        protocol.connection_lost(None)\n\n        # Task should NOT be cancelled immediately\n        mock_task.cancel.assert_not_called()\n\n    def test_disconnect_schedules_cancellation(self, mock_worker):\n        \"\"\"Test that connection_lost schedules task cancellation.\"\"\"\n        # Use a mock loop for this test to verify call_later was called\n        mock_loop = mock.Mock()\n        mock_worker.loop = mock_loop\n\n        protocol = ASGIProtocol(mock_worker)\n        protocol.reader = mock.Mock()\n        mock_worker.nr_conns = 1\n\n        # Create a mock task\n        mock_task = mock.Mock()\n        mock_task.done.return_value = False\n        protocol._task = mock_task\n\n        # Simulate connection lost\n        protocol.connection_lost(None)\n\n        # call_later should have been called to schedule cancellation\n        mock_loop.call_later.assert_called_once()\n        args = mock_loop.call_later.call_args[0]\n        assert args[0] == mock_worker.cfg.asgi_disconnect_grace_period\n        assert args[1] == protocol._cancel_task_if_pending\n\n    def test_cancel_task_if_pending_cancels_running_task(self, mock_worker):\n        \"\"\"Test that _cancel_task_if_pending cancels a running task.\"\"\"\n        protocol = ASGIProtocol(mock_worker)\n\n        # Create a mock task that's still running\n        mock_task = mock.Mock()\n        mock_task.done.return_value = False\n        protocol._task = mock_task\n\n        protocol._cancel_task_if_pending()\n\n        mock_task.cancel.assert_called_once()\n\n    def test_cancel_task_if_pending_skips_completed_task(self, mock_worker):\n        \"\"\"Test that _cancel_task_if_pending doesn't cancel completed tasks.\"\"\"\n        protocol = ASGIProtocol(mock_worker)\n\n        # Create a mock task that's already done\n        mock_task = mock.Mock()\n        mock_task.done.return_value = True\n        protocol._task = mock_task\n\n        protocol._cancel_task_if_pending()\n\n        mock_task.cancel.assert_not_called()\n\n    @pytest.mark.asyncio\n    async def test_receive_returns_disconnect_when_closed(self, mock_worker):\n        \"\"\"Test that receive() returns http.disconnect when connection is closed.\"\"\"\n        protocol = ASGIProtocol(mock_worker)\n        protocol._closed = True\n\n        # Create receive queue with body complete\n        receive_queue = asyncio.Queue()\n        protocol._receive_queue = receive_queue\n\n        # Add initial body message\n        await receive_queue.put({\n            \"type\": \"http.request\",\n            \"body\": b\"\",\n            \"more_body\": False,\n        })\n\n        # Simulate what happens in _handle_http_request\n        body_complete = False\n\n        async def receive():\n            nonlocal body_complete\n            if protocol._closed and body_complete:\n                return {\"type\": \"http.disconnect\"}\n\n            msg = await receive_queue.get()\n\n            if msg.get(\"type\") == \"http.request\" and not msg.get(\"more_body\", True):\n                body_complete = True\n\n            return msg\n\n        # First receive gets the body\n        msg1 = await receive()\n        assert msg1[\"type\"] == \"http.request\"\n\n        # Second receive should get disconnect\n        msg2 = await receive()\n        assert msg2[\"type\"] == \"http.disconnect\"\n\n\nclass TestASGIDisconnectGracePeriod:\n    \"\"\"Test the grace period configuration.\"\"\"\n\n    def test_default_grace_period(self):\n        \"\"\"Test that the default grace period is reasonable.\"\"\"\n        from gunicorn.config import Config\n        cfg = Config()\n        assert cfg.asgi_disconnect_grace_period == 3\n"
  },
  {
    "path": "tests/test_asgi_http_scope.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nASGI HTTP scope validation tests.\n\nTests for HTTP scope building, URL encoding, header handling,\nand extension support.\n\"\"\"\n\nfrom unittest import mock\n\nimport pytest\n\nfrom gunicorn.config import Config\n\n\n# ============================================================================\n# HTTP Scope Building Tests\n# ============================================================================\n\nclass TestHTTPScopeBuilding:\n    \"\"\"Tests for _build_http_scope method.\"\"\"\n\n    def _create_protocol(self, **config_kwargs):\n        \"\"\"Create an ASGIProtocol instance for testing.\"\"\"\n        from gunicorn.asgi.protocol import ASGIProtocol\n\n        worker = mock.Mock()\n        worker.cfg = Config()\n        for key, value in config_kwargs.items():\n            worker.cfg.set(key, value)\n        worker.log = mock.Mock()\n        worker.asgi = mock.Mock()\n\n        return ASGIProtocol(worker)\n\n    def _create_mock_request(self, **kwargs):\n        \"\"\"Create a mock HTTP request.\"\"\"\n        request = mock.Mock()\n        request.method = kwargs.get(\"method\", \"GET\")\n        request.path = kwargs.get(\"path\", \"/\")\n        request.query = kwargs.get(\"query\", \"\")\n        request.version = kwargs.get(\"version\", (1, 1))\n        request.scheme = kwargs.get(\"scheme\", \"http\")\n        request.headers = kwargs.get(\"headers\", [])\n\n        # Optionally add HTTP/2 priority attributes\n        if \"priority_weight\" in kwargs:\n            request.priority_weight = kwargs[\"priority_weight\"]\n            request.priority_depends_on = kwargs.get(\"priority_depends_on\", 0)\n\n        return request\n\n    def test_basic_scope_structure(self):\n        \"\"\"Test basic HTTP scope structure.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request()\n\n        scope = protocol._build_http_scope(\n            request,\n            (\"127.0.0.1\", 8000),\n            (\"192.168.1.100\", 54321),\n        )\n\n        # All required keys should be present\n        required_keys = [\n            \"type\", \"asgi\", \"http_version\", \"method\", \"scheme\",\n            \"path\", \"raw_path\", \"query_string\", \"root_path\",\n            \"headers\", \"server\", \"client\",\n        ]\n        for key in required_keys:\n            assert key in scope, f\"Missing required key: {key}\"\n\n    def test_root_path_configuration(self):\n        \"\"\"Test root_path from configuration.\"\"\"\n        protocol = self._create_protocol(root_path=\"/api/v1\")\n        request = self._create_mock_request()\n\n        scope = protocol._build_http_scope(request, None, None)\n\n        assert scope[\"root_path\"] == \"/api/v1\"\n\n    def test_root_path_default_empty(self):\n        \"\"\"Test root_path defaults to empty string.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request()\n\n        scope = protocol._build_http_scope(request, None, None)\n\n        assert scope[\"root_path\"] == \"\"\n\n\n# ============================================================================\n# Path Handling Tests\n# ============================================================================\n\nclass TestPathHandling:\n    \"\"\"Tests for path handling in HTTP scope.\"\"\"\n\n    def _create_protocol(self):\n        \"\"\"Create an ASGIProtocol instance for testing.\"\"\"\n        from gunicorn.asgi.protocol import ASGIProtocol\n\n        worker = mock.Mock()\n        worker.cfg = Config()\n        worker.log = mock.Mock()\n        worker.asgi = mock.Mock()\n\n        return ASGIProtocol(worker)\n\n    def _create_mock_request(self, **kwargs):\n        \"\"\"Create a mock HTTP request.\"\"\"\n        request = mock.Mock()\n        request.method = kwargs.get(\"method\", \"GET\")\n        request.path = kwargs.get(\"path\", \"/\")\n        request.query = kwargs.get(\"query\", \"\")\n        request.version = kwargs.get(\"version\", (1, 1))\n        request.scheme = kwargs.get(\"scheme\", \"http\")\n        request.headers = kwargs.get(\"headers\", [])\n        return request\n\n    def test_simple_path(self):\n        \"\"\"Test simple path handling.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(path=\"/users\")\n\n        scope = protocol._build_http_scope(request, None, None)\n\n        assert scope[\"path\"] == \"/users\"\n        assert scope[\"raw_path\"] == b\"/users\"\n\n    def test_path_with_unicode(self):\n        \"\"\"Test path with unicode characters.\"\"\"\n        protocol = self._create_protocol()\n        # Latin-1 encodable characters\n        request = self._create_mock_request(path=\"/caf\\xe9\")\n\n        scope = protocol._build_http_scope(request, None, None)\n\n        assert scope[\"path\"] == \"/caf\\xe9\"\n        assert scope[\"raw_path\"] == b\"/caf\\xe9\"\n\n    def test_nested_path(self):\n        \"\"\"Test nested path handling.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(path=\"/api/v1/users/123/posts\")\n\n        scope = protocol._build_http_scope(request, None, None)\n\n        assert scope[\"path\"] == \"/api/v1/users/123/posts\"\n\n    def test_root_path_only(self):\n        \"\"\"Test root path only.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(path=\"/\")\n\n        scope = protocol._build_http_scope(request, None, None)\n\n        assert scope[\"path\"] == \"/\"\n        assert scope[\"raw_path\"] == b\"/\"\n\n    def test_empty_path(self):\n        \"\"\"Test empty path handling.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(path=\"\")\n\n        scope = protocol._build_http_scope(request, None, None)\n\n        assert scope[\"path\"] == \"\"\n        assert scope[\"raw_path\"] == b\"\"\n\n\n# ============================================================================\n# Query String Tests\n# ============================================================================\n\nclass TestQueryStringHandling:\n    \"\"\"Tests for query string handling in HTTP scope.\"\"\"\n\n    def _create_protocol(self):\n        \"\"\"Create an ASGIProtocol instance for testing.\"\"\"\n        from gunicorn.asgi.protocol import ASGIProtocol\n\n        worker = mock.Mock()\n        worker.cfg = Config()\n        worker.log = mock.Mock()\n        worker.asgi = mock.Mock()\n\n        return ASGIProtocol(worker)\n\n    def _create_mock_request(self, **kwargs):\n        \"\"\"Create a mock HTTP request.\"\"\"\n        request = mock.Mock()\n        request.method = kwargs.get(\"method\", \"GET\")\n        request.path = kwargs.get(\"path\", \"/\")\n        request.query = kwargs.get(\"query\", \"\")\n        request.version = kwargs.get(\"version\", (1, 1))\n        request.scheme = kwargs.get(\"scheme\", \"http\")\n        request.headers = kwargs.get(\"headers\", [])\n        return request\n\n    def test_simple_query_string(self):\n        \"\"\"Test simple query string.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(query=\"page=1\")\n\n        scope = protocol._build_http_scope(request, None, None)\n\n        assert scope[\"query_string\"] == b\"page=1\"\n\n    def test_multiple_query_params(self):\n        \"\"\"Test multiple query parameters.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(query=\"page=1&limit=10&sort=name\")\n\n        scope = protocol._build_http_scope(request, None, None)\n\n        assert scope[\"query_string\"] == b\"page=1&limit=10&sort=name\"\n\n    def test_empty_query_string(self):\n        \"\"\"Test empty query string.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(query=\"\")\n\n        scope = protocol._build_http_scope(request, None, None)\n\n        assert scope[\"query_string\"] == b\"\"\n\n    def test_query_with_special_characters(self):\n        \"\"\"Test query string with special characters.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(query=\"name=John%20Doe&email=test%40example.com\")\n\n        scope = protocol._build_http_scope(request, None, None)\n\n        # Query string should be preserved as-is (URL encoded)\n        assert scope[\"query_string\"] == b\"name=John%20Doe&email=test%40example.com\"\n\n    def test_query_with_unicode(self):\n        \"\"\"Test query string with unicode (Latin-1 encodable).\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(query=\"city=caf\\xe9\")\n\n        scope = protocol._build_http_scope(request, None, None)\n\n        assert scope[\"query_string\"] == b\"city=caf\\xe9\"\n\n\n# ============================================================================\n# Header Handling Tests\n# ============================================================================\n\nclass TestHeaderHandling:\n    \"\"\"Tests for header handling in HTTP scope.\"\"\"\n\n    def _create_protocol(self):\n        \"\"\"Create an ASGIProtocol instance for testing.\"\"\"\n        from gunicorn.asgi.protocol import ASGIProtocol\n\n        worker = mock.Mock()\n        worker.cfg = Config()\n        worker.log = mock.Mock()\n        worker.asgi = mock.Mock()\n\n        return ASGIProtocol(worker)\n\n    def _create_mock_request(self, **kwargs):\n        \"\"\"Create a mock HTTP request.\"\"\"\n        request = mock.Mock()\n        request.method = kwargs.get(\"method\", \"GET\")\n        request.path = kwargs.get(\"path\", \"/\")\n        request.query = kwargs.get(\"query\", \"\")\n        request.version = kwargs.get(\"version\", (1, 1))\n        request.scheme = kwargs.get(\"scheme\", \"http\")\n        request.headers = kwargs.get(\"headers\", [])\n        return request\n\n    def test_headers_converted_to_bytes(self):\n        \"\"\"Test that headers are converted to bytes tuples.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(\n            headers=[(\"HOST\", \"localhost\"), (\"ACCEPT\", \"text/html\")]\n        )\n\n        scope = protocol._build_http_scope(request, None, None)\n\n        for name, value in scope[\"headers\"]:\n            assert isinstance(name, bytes)\n            assert isinstance(value, bytes)\n\n    def test_headers_lowercase(self):\n        \"\"\"Test that header names are lowercased.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(\n            headers=[(\"HOST\", \"localhost\"), (\"Content-Type\", \"application/json\")]\n        )\n\n        scope = protocol._build_http_scope(request, None, None)\n\n        header_names = [name for name, _ in scope[\"headers\"]]\n        assert b\"host\" in header_names\n        assert b\"content-type\" in header_names\n\n    def test_multiple_headers_same_name(self):\n        \"\"\"Test multiple headers with the same name.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(\n            headers=[\n                (\"ACCEPT\", \"text/html\"),\n                (\"ACCEPT\", \"application/json\"),\n            ]\n        )\n\n        scope = protocol._build_http_scope(request, None, None)\n\n        accept_headers = [value for name, value in scope[\"headers\"] if name == b\"accept\"]\n        assert len(accept_headers) == 2\n\n    def test_empty_headers(self):\n        \"\"\"Test empty headers list.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(headers=[])\n\n        scope = protocol._build_http_scope(request, None, None)\n\n        assert scope[\"headers\"] == []\n\n    def test_header_value_with_special_chars(self):\n        \"\"\"Test header values with special characters.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(\n            headers=[(\"USER-AGENT\", \"Mozilla/5.0 (compatible; bot/1.0)\")]\n        )\n\n        scope = protocol._build_http_scope(request, None, None)\n\n        user_agent = [v for n, v in scope[\"headers\"] if n == b\"user-agent\"][0]\n        assert user_agent == b\"Mozilla/5.0 (compatible; bot/1.0)\"\n\n\n# ============================================================================\n# WebSocket Scope Tests\n# ============================================================================\n\nclass TestWebSocketScope:\n    \"\"\"Tests for WebSocket scope building.\"\"\"\n\n    def _create_protocol(self):\n        \"\"\"Create an ASGIProtocol instance for testing.\"\"\"\n        from gunicorn.asgi.protocol import ASGIProtocol\n\n        worker = mock.Mock()\n        worker.cfg = Config()\n        worker.log = mock.Mock()\n        worker.asgi = mock.Mock()\n\n        return ASGIProtocol(worker)\n\n    def _create_mock_request(self, **kwargs):\n        \"\"\"Create a mock WebSocket upgrade request.\"\"\"\n        request = mock.Mock()\n        request.method = \"GET\"\n        request.path = kwargs.get(\"path\", \"/ws\")\n        request.query = kwargs.get(\"query\", \"\")\n        request.version = kwargs.get(\"version\", (1, 1))\n        request.scheme = kwargs.get(\"scheme\", \"http\")\n        request.headers = kwargs.get(\"headers\", [\n            (\"HOST\", \"localhost\"),\n            (\"UPGRADE\", \"websocket\"),\n            (\"CONNECTION\", \"upgrade\"),\n            (\"SEC-WEBSOCKET-KEY\", \"dGhlIHNhbXBsZSBub25jZQ==\"),\n            (\"SEC-WEBSOCKET-VERSION\", \"13\"),\n        ])\n        return request\n\n    def test_websocket_scope_type(self):\n        \"\"\"Test WebSocket scope type.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request()\n\n        scope = protocol._build_websocket_scope(\n            request,\n            (\"127.0.0.1\", 8000),\n            (\"127.0.0.1\", 12345),\n        )\n\n        assert scope[\"type\"] == \"websocket\"\n\n    def test_websocket_scheme_ws(self):\n        \"\"\"Test WebSocket scheme for HTTP.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(scheme=\"http\")\n\n        scope = protocol._build_websocket_scope(request, None, None)\n\n        assert scope[\"scheme\"] == \"ws\"\n\n    def test_websocket_scheme_wss(self):\n        \"\"\"Test WebSocket scheme for HTTPS.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(scheme=\"https\")\n\n        scope = protocol._build_websocket_scope(request, None, None)\n\n        assert scope[\"scheme\"] == \"wss\"\n\n    def test_websocket_subprotocols(self):\n        \"\"\"Test WebSocket subprotocol extraction.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(\n            headers=[\n                (\"HOST\", \"localhost\"),\n                (\"UPGRADE\", \"websocket\"),\n                (\"CONNECTION\", \"upgrade\"),\n                (\"SEC-WEBSOCKET-KEY\", \"dGhlIHNhbXBsZSBub25jZQ==\"),\n                (\"SEC-WEBSOCKET-VERSION\", \"13\"),\n                (\"SEC-WEBSOCKET-PROTOCOL\", \"graphql-ws, subscriptions-transport-ws\"),\n            ]\n        )\n\n        scope = protocol._build_websocket_scope(request, None, None)\n\n        assert \"subprotocols\" in scope\n        assert \"graphql-ws\" in scope[\"subprotocols\"]\n        assert \"subscriptions-transport-ws\" in scope[\"subprotocols\"]\n\n    def test_websocket_no_subprotocols(self):\n        \"\"\"Test WebSocket scope without subprotocols.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request()\n\n        scope = protocol._build_websocket_scope(request, None, None)\n\n        assert \"subprotocols\" in scope\n        assert scope[\"subprotocols\"] == []\n\n    def test_websocket_asgi_version(self):\n        \"\"\"Test ASGI version in WebSocket scope.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request()\n\n        scope = protocol._build_websocket_scope(request, None, None)\n\n        assert \"asgi\" in scope\n        assert scope[\"asgi\"][\"version\"] == \"3.0\"\n\n    def test_websocket_required_keys(self):\n        \"\"\"Test all required keys are present in WebSocket scope.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request()\n\n        scope = protocol._build_websocket_scope(\n            request,\n            (\"127.0.0.1\", 8000),\n            (\"127.0.0.1\", 12345),\n        )\n\n        required_keys = [\n            \"type\", \"asgi\", \"http_version\", \"scheme\",\n            \"path\", \"raw_path\", \"query_string\", \"root_path\",\n            \"headers\", \"server\", \"client\", \"subprotocols\",\n        ]\n        for key in required_keys:\n            assert key in scope, f\"Missing required key: {key}\"\n\n\n# ============================================================================\n# HTTP/2 Scope Tests\n# ============================================================================\n\nclass TestHTTP2Scope:\n    \"\"\"Tests for HTTP/2 scope building.\"\"\"\n\n    def _create_protocol(self):\n        \"\"\"Create an ASGIProtocol instance for testing.\"\"\"\n        from gunicorn.asgi.protocol import ASGIProtocol\n\n        worker = mock.Mock()\n        worker.cfg = Config()\n        worker.log = mock.Mock()\n        worker.asgi = mock.Mock()\n\n        return ASGIProtocol(worker)\n\n    def _create_mock_http2_request(self, **kwargs):\n        \"\"\"Create a mock HTTP/2 request.\"\"\"\n        request = mock.Mock()\n        request.method = kwargs.get(\"method\", \"GET\")\n        request.path = kwargs.get(\"path\", \"/\")\n        request.query = kwargs.get(\"query\", \"\")\n        request.uri = kwargs.get(\"uri\", \"/\")\n        request.scheme = kwargs.get(\"scheme\", \"https\")\n        request.headers = kwargs.get(\"headers\", [])\n        request.priority_weight = kwargs.get(\"priority_weight\", 16)\n        request.priority_depends_on = kwargs.get(\"priority_depends_on\", 0)\n        return request\n\n    def test_http2_version_string(self):\n        \"\"\"Test HTTP/2 version string in scope.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_http2_request()\n\n        scope = protocol._build_http2_scope(request, None, None)\n\n        assert scope[\"http_version\"] == \"2\"\n\n    def test_http2_priority_extension(self):\n        \"\"\"Test HTTP/2 priority extension.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_http2_request(\n            priority_weight=256,\n            priority_depends_on=5,\n        )\n\n        scope = protocol._build_http2_scope(request, None, None)\n\n        assert \"extensions\" in scope\n        assert \"http.response.priority\" in scope[\"extensions\"]\n        priority = scope[\"extensions\"][\"http.response.priority\"]\n        assert priority[\"weight\"] == 256\n        assert priority[\"depends_on\"] == 5\n\n    def test_http2_trailers_extension(self):\n        \"\"\"Test HTTP/2 trailers extension present.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_http2_request()\n\n        scope = protocol._build_http2_scope(request, None, None)\n\n        assert \"extensions\" in scope\n        assert \"http.response.trailers\" in scope[\"extensions\"]\n\n    def test_http2_scope_required_keys(self):\n        \"\"\"Test all required keys in HTTP/2 scope.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_http2_request()\n\n        scope = protocol._build_http2_scope(\n            request,\n            (\"127.0.0.1\", 8443),\n            (\"127.0.0.1\", 12345),\n        )\n\n        required_keys = [\n            \"type\", \"asgi\", \"http_version\", \"method\", \"scheme\",\n            \"path\", \"raw_path\", \"query_string\", \"root_path\",\n            \"headers\", \"server\", \"client\", \"extensions\",\n        ]\n        for key in required_keys:\n            assert key in scope, f\"Missing required key: {key}\"\n\n\n# ============================================================================\n# Server/Client Address Tests\n# ============================================================================\n\nclass TestAddressHandling:\n    \"\"\"Tests for server and client address handling.\"\"\"\n\n    def _create_protocol(self):\n        \"\"\"Create an ASGIProtocol instance for testing.\"\"\"\n        from gunicorn.asgi.protocol import ASGIProtocol\n\n        worker = mock.Mock()\n        worker.cfg = Config()\n        worker.log = mock.Mock()\n        worker.asgi = mock.Mock()\n\n        return ASGIProtocol(worker)\n\n    def _create_mock_request(self):\n        \"\"\"Create a mock HTTP request.\"\"\"\n        request = mock.Mock()\n        request.method = \"GET\"\n        request.path = \"/\"\n        request.query = \"\"\n        request.version = (1, 1)\n        request.scheme = \"http\"\n        request.headers = []\n        return request\n\n    def test_ipv4_addresses(self):\n        \"\"\"Test IPv4 server and client addresses.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request()\n\n        scope = protocol._build_http_scope(\n            request,\n            (\"192.168.1.1\", 8000),\n            (\"192.168.1.100\", 54321),\n        )\n\n        assert scope[\"server\"] == (\"192.168.1.1\", 8000)\n        assert scope[\"client\"] == (\"192.168.1.100\", 54321)\n\n    def test_ipv6_addresses(self):\n        \"\"\"Test IPv6 server and client addresses.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request()\n\n        scope = protocol._build_http_scope(\n            request,\n            (\"::1\", 8000),\n            (\"::1\", 54321),\n        )\n\n        assert scope[\"server\"] == (\"::1\", 8000)\n        assert scope[\"client\"] == (\"::1\", 54321)\n\n    def test_localhost_addresses(self):\n        \"\"\"Test localhost addresses.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request()\n\n        scope = protocol._build_http_scope(\n            request,\n            (\"127.0.0.1\", 8000),\n            (\"127.0.0.1\", 12345),\n        )\n\n        assert scope[\"server\"] == (\"127.0.0.1\", 8000)\n        assert scope[\"client\"] == (\"127.0.0.1\", 12345)\n\n    def test_addresses_none(self):\n        \"\"\"Test when addresses are not available.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request()\n\n        scope = protocol._build_http_scope(request, None, None)\n\n        assert scope[\"server\"] is None\n        assert scope[\"client\"] is None\n\n\n# ============================================================================\n# Environ Building Tests (for access logging)\n# ============================================================================\n\nclass TestEnvironBuilding:\n    \"\"\"Tests for environ dict building (used for access logging).\"\"\"\n\n    def _create_protocol(self):\n        \"\"\"Create an ASGIProtocol instance for testing.\"\"\"\n        from gunicorn.asgi.protocol import ASGIProtocol\n\n        worker = mock.Mock()\n        worker.cfg = Config()\n        worker.log = mock.Mock()\n        worker.asgi = mock.Mock()\n\n        return ASGIProtocol(worker)\n\n    def _create_mock_request(self, **kwargs):\n        \"\"\"Create a mock HTTP request.\"\"\"\n        request = mock.Mock()\n        request.method = kwargs.get(\"method\", \"GET\")\n        request.path = kwargs.get(\"path\", \"/\")\n        request.query = kwargs.get(\"query\", \"\")\n        request.uri = kwargs.get(\"uri\", \"/\")\n        request.version = kwargs.get(\"version\", (1, 1))\n        request.scheme = kwargs.get(\"scheme\", \"http\")\n        request.headers = kwargs.get(\"headers\", [])\n        return request\n\n    def test_environ_request_method(self):\n        \"\"\"Test REQUEST_METHOD in environ.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(method=\"POST\")\n\n        environ = protocol._build_environ(\n            request,\n            (\"127.0.0.1\", 8000),\n            (\"127.0.0.1\", 12345),\n        )\n\n        assert environ[\"REQUEST_METHOD\"] == \"POST\"\n\n    def test_environ_raw_uri(self):\n        \"\"\"Test RAW_URI in environ.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(uri=\"/api/users?page=1\")\n\n        environ = protocol._build_environ(request, None, None)\n\n        assert environ[\"RAW_URI\"] == \"/api/users?page=1\"\n\n    def test_environ_path_info(self):\n        \"\"\"Test PATH_INFO in environ.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(path=\"/api/users\")\n\n        environ = protocol._build_environ(request, None, None)\n\n        assert environ[\"PATH_INFO\"] == \"/api/users\"\n\n    def test_environ_query_string(self):\n        \"\"\"Test QUERY_STRING in environ.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(query=\"page=1&limit=10\")\n\n        environ = protocol._build_environ(request, None, None)\n\n        assert environ[\"QUERY_STRING\"] == \"page=1&limit=10\"\n\n    def test_environ_server_protocol(self):\n        \"\"\"Test SERVER_PROTOCOL in environ.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(version=(1, 1))\n\n        environ = protocol._build_environ(request, None, None)\n\n        assert environ[\"SERVER_PROTOCOL\"] == \"HTTP/1.1\"\n\n    def test_environ_remote_addr(self):\n        \"\"\"Test REMOTE_ADDR in environ.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request()\n\n        environ = protocol._build_environ(\n            request,\n            None,\n            (\"192.168.1.100\", 54321),\n        )\n\n        assert environ[\"REMOTE_ADDR\"] == \"192.168.1.100\"\n\n    def test_environ_remote_addr_missing(self):\n        \"\"\"Test REMOTE_ADDR when peername is None.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request()\n\n        environ = protocol._build_environ(request, None, None)\n\n        assert environ[\"REMOTE_ADDR\"] == \"-\"\n\n    def test_environ_http_headers(self):\n        \"\"\"Test HTTP headers in environ.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(\n            headers=[\n                (\"HOST\", \"localhost:8000\"),\n                (\"USER-AGENT\", \"TestClient/1.0\"),\n                (\"ACCEPT\", \"application/json\"),\n            ]\n        )\n\n        environ = protocol._build_environ(request, None, None)\n\n        assert environ[\"HTTP_HOST\"] == \"localhost:8000\"\n        # Header names have dashes converted to underscores in environ\n        assert environ[\"HTTP_USER_AGENT\"] == \"TestClient/1.0\"\n        assert environ[\"HTTP_ACCEPT\"] == \"application/json\"\n"
  },
  {
    "path": "tests/test_asgi_parser.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nTests for ASGI HTTP parser optimizations.\n\"\"\"\n\nimport asyncio\nimport ipaddress\nimport pytest\n\nfrom gunicorn.asgi.unreader import AsyncUnreader\nfrom gunicorn.asgi.message import AsyncRequest\n\n\nclass MockStreamReader:\n    \"\"\"Mock asyncio.StreamReader for testing.\"\"\"\n\n    def __init__(self, data):\n        self.data = data\n        self.pos = 0\n\n    async def read(self, size=-1):\n        if self.pos >= len(self.data):\n            return b\"\"\n        if size < 0:\n            result = self.data[self.pos:]\n            self.pos = len(self.data)\n        else:\n            result = self.data[self.pos:self.pos + size]\n            self.pos += size\n        return result\n\n\nclass MockConfig:\n    \"\"\"Mock gunicorn config for testing.\"\"\"\n\n    def __init__(self):\n        self.is_ssl = False\n        self.proxy_protocol = \"off\"\n        self.proxy_allow_ips = [\"127.0.0.1\"]\n        self.forwarded_allow_ips = [\"127.0.0.1\"]\n        self._proxy_allow_networks = None\n        self._forwarded_allow_networks = None\n        self.secure_scheme_headers = {}\n        self.forwarder_headers = []\n        self.limit_request_line = 8190\n        self.limit_request_fields = 100\n        self.limit_request_field_size = 8190\n        self.permit_unconventional_http_method = False\n        self.permit_unconventional_http_version = False\n        self.permit_obsolete_folding = False\n        self.casefold_http_method = False\n        self.strip_header_spaces = False\n        self.header_map = \"refuse\"\n\n    def forwarded_allow_networks(self):\n        if self._forwarded_allow_networks is None:\n            self._forwarded_allow_networks = [\n                ipaddress.ip_network(addr)\n                for addr in self.forwarded_allow_ips\n                if addr != \"*\"\n            ]\n        return self._forwarded_allow_networks\n\n    def proxy_allow_networks(self):\n        if self._proxy_allow_networks is None:\n            self._proxy_allow_networks = [\n                ipaddress.ip_network(addr)\n                for addr in self.proxy_allow_ips\n                if addr != \"*\"\n            ]\n        return self._proxy_allow_networks\n\n\n# Optimized Chunk Reading Tests\n\n@pytest.mark.asyncio\nasync def test_chunk_size_line_reading():\n    \"\"\"Test optimized chunk size line reading.\"\"\"\n    # Simulate chunked body with chunk size line\n    data = b\"a\\r\\nhello body\\r\\n0\\r\\n\\r\\n\"\n    reader = MockStreamReader(data)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    req = AsyncRequest(cfg, unreader, (\"127.0.0.1\", 8000))\n    # Access the private method for testing\n    line = await req._read_chunk_size_line()\n    assert line == b\"a\"\n\n\n@pytest.mark.asyncio\nasync def test_skip_trailers_empty():\n    \"\"\"Test skipping empty trailers.\"\"\"\n    data = b\"\\r\\n\"\n    reader = MockStreamReader(data)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    req = AsyncRequest(cfg, unreader, (\"127.0.0.1\", 8000))\n    # Should not raise\n    await req._skip_trailers()\n\n\n@pytest.mark.asyncio\nasync def test_skip_trailers_with_headers():\n    \"\"\"Test skipping trailers with actual headers.\"\"\"\n    data = b\"X-Checksum: abc123\\r\\n\\r\\n\"\n    reader = MockStreamReader(data)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    req = AsyncRequest(cfg, unreader, (\"127.0.0.1\", 8000))\n    # Should not raise\n    await req._skip_trailers()\n\n\n# Buffer Reuse Tests\n\n@pytest.mark.asyncio\nasync def test_unreader_buffer_reuse():\n    \"\"\"Test that AsyncUnreader reuses buffers efficiently.\"\"\"\n    data = b\"GET / HTTP/1.1\\r\\nHost: example.com\\r\\n\\r\\n\"\n    reader = MockStreamReader(data)\n    unreader = AsyncUnreader(reader)\n\n    # Read in chunks\n    chunk1 = await unreader.read(10)\n    assert chunk1 == b\"GET / HTTP\"\n\n    # Read more\n    chunk2 = await unreader.read(10)\n    assert chunk2 == b\"/1.1\\r\\nHost\"\n\n    # Unread some data\n    unreader.unread(b\"/1.1\\r\\nHost\")\n\n    # Read again - should get unreaded data\n    chunk3 = await unreader.read(10)\n    assert chunk3 == b\"/1.1\\r\\nHost\"\n\n\n@pytest.mark.asyncio\nasync def test_unreader_unread_prepends():\n    \"\"\"Test that unread prepends data.\"\"\"\n    data = b\"original\"\n    reader = MockStreamReader(data)\n    unreader = AsyncUnreader(reader)\n\n    # Read some data first\n    await unreader.read(4)  # \"orig\"\n\n    # Unread something different\n    unreader.unread(b\"NEW\")\n\n    # Should read the new data first\n    result = await unreader.read(3)\n    assert result == b\"NEW\"\n\n\n# Header Parsing Optimization Tests\n\n@pytest.mark.asyncio\nasync def test_header_parsing_index_iteration():\n    \"\"\"Test that header parsing uses index-based iteration.\"\"\"\n    raw_request = (\n        b\"GET / HTTP/1.1\\r\\n\"\n        b\"Host: example.com\\r\\n\"\n        b\"Content-Type: text/plain\\r\\n\"\n        b\"X-Custom: value\\r\\n\"\n        b\"\\r\\n\"\n    )\n    reader = MockStreamReader(raw_request)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    req = await AsyncRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n\n    assert req.method == \"GET\"\n    assert req.path == \"/\"\n    assert len(req.headers) == 3\n    assert (\"HOST\", \"example.com\") in req.headers\n    assert (\"CONTENT-TYPE\", \"text/plain\") in req.headers\n    assert (\"X-CUSTOM\", \"value\") in req.headers\n\n\n@pytest.mark.asyncio\nasync def test_many_headers_performance():\n    \"\"\"Test parsing request with many headers.\"\"\"\n    headers = []\n    for i in range(50):\n        headers.append(f\"X-Header-{i}: value-{i}\\r\\n\")\n\n    raw_request = (\n        b\"GET / HTTP/1.1\\r\\n\"\n        + \"\".join(headers).encode()\n        + b\"\\r\\n\"\n    )\n\n    reader = MockStreamReader(raw_request)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    req = await AsyncRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n\n    assert len(req.headers) == 50\n\n\n# Bytearray Find Optimization Tests\n\n@pytest.mark.asyncio\nasync def test_bytearray_find_optimization():\n    \"\"\"Test that bytearray.find() is used instead of bytes().find().\"\"\"\n    raw_request = (\n        b\"GET /path?query=value HTTP/1.1\\r\\n\"\n        b\"Host: example.com\\r\\n\"\n        b\"Content-Length: 5\\r\\n\"\n        b\"\\r\\n\"\n        b\"hello\"\n    )\n    reader = MockStreamReader(raw_request)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    req = await AsyncRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n\n    assert req.method == \"GET\"\n    assert req.path == \"/path\"\n    assert req.query == \"query=value\"\n    assert req.content_length == 5\n\n\n# Chunked Body Tests with Optimized Reading\n\n@pytest.mark.asyncio\nasync def test_chunked_body_optimized_reading():\n    \"\"\"Test reading chunked body with optimized chunk reading.\"\"\"\n    raw_request = (\n        b\"POST / HTTP/1.1\\r\\n\"\n        b\"Host: example.com\\r\\n\"\n        b\"Transfer-Encoding: chunked\\r\\n\"\n        b\"\\r\\n\"\n        b\"5\\r\\nhello\\r\\n\"\n        b\"6\\r\\n world\\r\\n\"\n        b\"0\\r\\n\\r\\n\"\n    )\n    reader = MockStreamReader(raw_request)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    req = await AsyncRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n\n    assert req.chunked is True\n    assert req.content_length is None\n\n    # Read body\n    body_parts = []\n    while True:\n        chunk = await req.read_body(1024)\n        if not chunk:\n            break\n        body_parts.append(chunk)\n\n    body = b\"\".join(body_parts)\n    assert body == b\"hello world\"\n\n\n@pytest.mark.asyncio\nasync def test_chunked_body_with_extension():\n    \"\"\"Test reading chunked body with chunk extensions.\"\"\"\n    raw_request = (\n        b\"POST / HTTP/1.1\\r\\n\"\n        b\"Host: example.com\\r\\n\"\n        b\"Transfer-Encoding: chunked\\r\\n\"\n        b\"\\r\\n\"\n        b\"5;ext=value\\r\\nhello\\r\\n\"\n        b\"0\\r\\n\\r\\n\"\n    )\n    reader = MockStreamReader(raw_request)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    req = await AsyncRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n\n    chunk = await req.read_body(1024)\n    assert chunk == b\"hello\"\n\n\n# Edge Cases\n\n@pytest.mark.asyncio\nasync def test_empty_headers():\n    \"\"\"Test request with no headers.\"\"\"\n    raw_request = (\n        b\"GET / HTTP/1.1\\r\\n\"\n        b\"\\r\\n\"\n    )\n    reader = MockStreamReader(raw_request)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    req = await AsyncRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n\n    assert req.method == \"GET\"\n    assert len(req.headers) == 0\n\n\n@pytest.mark.asyncio\nasync def test_large_header_value():\n    \"\"\"Test request with large header value.\"\"\"\n    large_value = \"x\" * 4000  # Within default limit\n    raw_request = (\n        b\"GET / HTTP/1.1\\r\\n\"\n        + f\"X-Large-Header: {large_value}\\r\\n\".encode()\n        + b\"\\r\\n\"\n    )\n    reader = MockStreamReader(raw_request)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    req = await AsyncRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n\n    assert req.get_header(\"X-Large-Header\") == large_value\n"
  },
  {
    "path": "tests/test_asgi_streaming.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nASGI streaming response tests.\n\nTests for chunked transfer encoding, Server-Sent Events (SSE),\nand streaming response handling.\n\"\"\"\n\nimport asyncio\nfrom unittest import mock\n\nimport pytest\n\nfrom gunicorn.config import Config\n\n\n# ============================================================================\n# Chunked Transfer Encoding Tests\n# ============================================================================\n\nclass TestChunkedTransferEncoding:\n    \"\"\"Tests for HTTP/1.1 chunked transfer encoding.\"\"\"\n\n    def test_chunked_encoding_format(self):\n        \"\"\"Test chunked encoding format: size in hex + CRLF + data + CRLF.\"\"\"\n        body = b\"Hello\"\n        chunk = f\"{len(body):x}\\r\\n\".encode(\"latin-1\") + body + b\"\\r\\n\"\n\n        assert chunk == b\"5\\r\\nHello\\r\\n\"\n\n    def test_chunked_encoding_large_chunk(self):\n        \"\"\"Test chunked encoding with larger data.\"\"\"\n        body = b\"x\" * 1000\n        chunk = f\"{len(body):x}\\r\\n\".encode(\"latin-1\") + body + b\"\\r\\n\"\n\n        # 1000 in hex is 3e8\n        assert chunk.startswith(b\"3e8\\r\\n\")\n        assert chunk.endswith(b\"\\r\\n\")\n\n    def test_chunked_encoding_terminal_chunk(self):\n        \"\"\"Test terminal chunk (zero-length).\"\"\"\n        terminal = b\"0\\r\\n\\r\\n\"\n\n        # Parse it\n        assert terminal == b\"0\\r\\n\\r\\n\"\n\n    def test_chunked_encoding_empty_chunk(self):\n        \"\"\"Test encoding empty body chunk.\"\"\"\n        body = b\"\"\n        chunk = f\"{len(body):x}\\r\\n\".encode(\"latin-1\") + body + b\"\\r\\n\"\n\n        assert chunk == b\"0\\r\\n\\r\\n\"\n\n    def test_chunked_encoding_multiple_chunks(self):\n        \"\"\"Test multiple chunks in sequence.\"\"\"\n        chunks = []\n\n        # First chunk\n        body1 = b\"Hello, \"\n        chunks.append(f\"{len(body1):x}\\r\\n\".encode() + body1 + b\"\\r\\n\")\n\n        # Second chunk\n        body2 = b\"World!\"\n        chunks.append(f\"{len(body2):x}\\r\\n\".encode() + body2 + b\"\\r\\n\")\n\n        # Terminal chunk\n        chunks.append(b\"0\\r\\n\\r\\n\")\n\n        full_response = b\"\".join(chunks)\n\n        assert b\"7\\r\\nHello, \\r\\n\" in full_response\n        assert b\"6\\r\\nWorld!\\r\\n\" in full_response\n        assert full_response.endswith(b\"0\\r\\n\\r\\n\")\n\n\n# ============================================================================\n# ASGI Streaming Response Tests\n# ============================================================================\n\nclass TestASGIStreamingResponse:\n    \"\"\"Tests for ASGI streaming response handling.\"\"\"\n\n    def test_streaming_response_more_body_true(self):\n        \"\"\"Test streaming response with more_body=True.\"\"\"\n        messages = [\n            {\n                \"type\": \"http.response.body\",\n                \"body\": b\"chunk1\",\n                \"more_body\": True,\n            },\n            {\n                \"type\": \"http.response.body\",\n                \"body\": b\"chunk2\",\n                \"more_body\": True,\n            },\n            {\n                \"type\": \"http.response.body\",\n                \"body\": b\"chunk3\",\n                \"more_body\": False,\n            },\n        ]\n\n        assert messages[0][\"more_body\"] is True\n        assert messages[1][\"more_body\"] is True\n        assert messages[2][\"more_body\"] is False\n\n    def test_streaming_response_empty_final_chunk(self):\n        \"\"\"Test streaming response with empty final chunk.\"\"\"\n        final_message = {\n            \"type\": \"http.response.body\",\n            \"body\": b\"\",\n            \"more_body\": False,\n        }\n\n        assert final_message[\"body\"] == b\"\"\n        assert final_message[\"more_body\"] is False\n\n    def test_response_start_without_content_length(self):\n        \"\"\"Test response start without Content-Length triggers chunked encoding.\"\"\"\n        # When Content-Length is missing, HTTP/1.1 should use chunked encoding\n        message = {\n            \"type\": \"http.response.start\",\n            \"status\": 200,\n            \"headers\": [\n                (b\"content-type\", b\"text/plain\"),\n                # No content-length header\n            ],\n        }\n\n        # Check no content-length in headers\n        header_names = [name.lower() for name, _ in message[\"headers\"]]\n        assert b\"content-length\" not in header_names\n\n\n# ============================================================================\n# Server-Sent Events (SSE) Format Tests\n# ============================================================================\n\nclass TestSSEFormat:\n    \"\"\"Tests for Server-Sent Events format.\"\"\"\n\n    def test_sse_data_event(self):\n        \"\"\"Test SSE data event format.\"\"\"\n        data = \"Hello, SSE!\"\n        event = f\"data: {data}\\n\\n\"\n\n        assert event == \"data: Hello, SSE!\\n\\n\"\n\n    def test_sse_named_event(self):\n        \"\"\"Test SSE named event format.\"\"\"\n        event_name = \"message\"\n        data = \"Hello\"\n        event = f\"event: {event_name}\\ndata: {data}\\n\\n\"\n\n        assert \"event: message\\n\" in event\n        assert \"data: Hello\\n\" in event\n        assert event.endswith(\"\\n\\n\")\n\n    def test_sse_event_with_id(self):\n        \"\"\"Test SSE event with ID.\"\"\"\n        event_id = \"12345\"\n        data = \"Some data\"\n        event = f\"id: {event_id}\\ndata: {data}\\n\\n\"\n\n        assert \"id: 12345\\n\" in event\n\n    def test_sse_multiline_data(self):\n        \"\"\"Test SSE multiline data.\"\"\"\n        lines = [\"line1\", \"line2\", \"line3\"]\n        data_lines = \"\\n\".join(f\"data: {line}\" for line in lines)\n        event = f\"{data_lines}\\n\\n\"\n\n        assert event == \"data: line1\\ndata: line2\\ndata: line3\\n\\n\"\n\n    def test_sse_retry_directive(self):\n        \"\"\"Test SSE retry directive.\"\"\"\n        retry_ms = 3000\n        directive = f\"retry: {retry_ms}\\n\\n\"\n\n        assert directive == \"retry: 3000\\n\\n\"\n\n    def test_sse_comment(self):\n        \"\"\"Test SSE comment (keep-alive).\"\"\"\n        comment = \": keep-alive\\n\\n\"\n\n        assert comment.startswith(\":\")\n\n    def test_sse_content_type(self):\n        \"\"\"Test SSE Content-Type header.\"\"\"\n        headers = [\n            (b\"content-type\", b\"text/event-stream\"),\n            (b\"cache-control\", b\"no-cache\"),\n            (b\"connection\", b\"keep-alive\"),\n        ]\n\n        content_type = dict(headers).get(b\"content-type\")\n        assert content_type == b\"text/event-stream\"\n\n\n# ============================================================================\n# Protocol Send Body Tests\n# ============================================================================\n\nclass TestProtocolSendBody:\n    \"\"\"Tests for ASGIProtocol._send_body method.\"\"\"\n\n    def _create_protocol(self):\n        \"\"\"Create an ASGIProtocol instance for testing.\"\"\"\n        from gunicorn.asgi.protocol import ASGIProtocol\n\n        worker = mock.Mock()\n        worker.cfg = Config()\n        worker.log = mock.Mock()\n        worker.asgi = mock.Mock()\n\n        protocol = ASGIProtocol(worker)\n        protocol.transport = mock.Mock()\n\n        return protocol\n\n    @pytest.mark.asyncio\n    async def test_send_body_without_chunking(self):\n        \"\"\"Test sending body without chunked encoding.\"\"\"\n        protocol = self._create_protocol()\n\n        await protocol._send_body(b\"Hello, World!\", chunked=False)\n\n        protocol.transport.write.assert_called_once_with(b\"Hello, World!\")\n\n    @pytest.mark.asyncio\n    async def test_send_body_with_chunking(self):\n        \"\"\"Test sending body with chunked encoding.\"\"\"\n        protocol = self._create_protocol()\n\n        await protocol._send_body(b\"Hello\", chunked=True)\n\n        # Should write: \"5\\r\\nHello\\r\\n\"\n        protocol.transport.write.assert_called_once()\n        call_arg = protocol.transport.write.call_args[0][0]\n        assert call_arg == b\"5\\r\\nHello\\r\\n\"\n\n    @pytest.mark.asyncio\n    async def test_send_body_empty_without_chunking(self):\n        \"\"\"Test sending empty body without chunked encoding.\"\"\"\n        protocol = self._create_protocol()\n\n        await protocol._send_body(b\"\", chunked=False)\n\n        # Empty body should not write anything\n        protocol.transport.write.assert_not_called()\n\n    @pytest.mark.asyncio\n    async def test_send_body_empty_with_chunking(self):\n        \"\"\"Test sending empty body with chunked encoding.\"\"\"\n        protocol = self._create_protocol()\n\n        await protocol._send_body(b\"\", chunked=True)\n\n        # Empty body should not write (terminal chunk handled separately)\n        protocol.transport.write.assert_not_called()\n\n\n# ============================================================================\n# Content-Length Detection Tests\n# ============================================================================\n\nclass TestContentLengthDetection:\n    \"\"\"Tests for Content-Length header detection.\"\"\"\n\n    def test_has_content_length_bytes(self):\n        \"\"\"Test detecting Content-Length header (bytes).\"\"\"\n        headers = [\n            (b\"content-type\", b\"text/plain\"),\n            (b\"content-length\", b\"100\"),\n        ]\n\n        has_cl = any(\n            name.lower() == b\"content-length\"\n            for name, _ in headers\n        )\n        assert has_cl is True\n\n    def test_has_content_length_string(self):\n        \"\"\"Test detecting Content-Length header (string).\"\"\"\n        headers = [\n            (\"content-type\", \"text/plain\"),\n            (\"content-length\", \"100\"),\n        ]\n\n        has_cl = any(\n            name.lower() == \"content-length\"\n            for name, _ in headers\n        )\n        assert has_cl is True\n\n    def test_no_content_length(self):\n        \"\"\"Test when Content-Length is missing.\"\"\"\n        headers = [\n            (b\"content-type\", b\"text/plain\"),\n        ]\n\n        has_cl = any(\n            name.lower() == b\"content-length\"\n            for name, _ in headers\n        )\n        assert has_cl is False\n\n    def test_content_length_case_insensitive(self):\n        \"\"\"Test Content-Length detection is case-insensitive.\"\"\"\n        headers = [\n            (b\"Content-Length\", b\"100\"),\n        ]\n\n        has_cl = any(\n            name.lower() == b\"content-length\"\n            for name, _ in headers\n        )\n        assert has_cl is True\n\n\n# ============================================================================\n# HTTP Version Check for Chunked Encoding\n# ============================================================================\n\nclass TestHTTPVersionForChunked:\n    \"\"\"Tests for HTTP version requirements for chunked encoding.\"\"\"\n\n    def test_http11_supports_chunked(self):\n        \"\"\"Test HTTP/1.1 supports chunked encoding.\"\"\"\n        version = (1, 1)\n        supports_chunked = version >= (1, 1)\n        assert supports_chunked is True\n\n    def test_http10_no_chunked(self):\n        \"\"\"Test HTTP/1.0 does not support chunked encoding.\"\"\"\n        version = (1, 0)\n        supports_chunked = version >= (1, 1)\n        assert supports_chunked is False\n\n    def test_http2_no_chunked(self):\n        \"\"\"Test HTTP/2 doesn't use chunked encoding (uses framing).\"\"\"\n        # HTTP/2 has its own framing mechanism\n        version = (2, 0)\n        # Chunked encoding is not used in HTTP/2\n        uses_http1_chunked = version[0] == 1 and version >= (1, 1)\n        assert uses_http1_chunked is False\n\n\n# ============================================================================\n# Streaming Response Message Sequence Tests\n# ============================================================================\n\nclass TestStreamingMessageSequence:\n    \"\"\"Tests for valid streaming response message sequences.\"\"\"\n\n    def test_valid_sequence_single_body(self):\n        \"\"\"Test valid sequence: start -> body (more_body=False).\"\"\"\n        messages = [\n            {\"type\": \"http.response.start\", \"status\": 200, \"headers\": []},\n            {\"type\": \"http.response.body\", \"body\": b\"Hello\", \"more_body\": False},\n        ]\n\n        # First message should be start\n        assert messages[0][\"type\"] == \"http.response.start\"\n        # Last body message should have more_body=False\n        assert messages[-1][\"type\"] == \"http.response.body\"\n        assert messages[-1][\"more_body\"] is False\n\n    def test_valid_sequence_multiple_bodies(self):\n        \"\"\"Test valid sequence: start -> body (more=True) -> body (more=False).\"\"\"\n        messages = [\n            {\"type\": \"http.response.start\", \"status\": 200, \"headers\": []},\n            {\"type\": \"http.response.body\", \"body\": b\"chunk1\", \"more_body\": True},\n            {\"type\": \"http.response.body\", \"body\": b\"chunk2\", \"more_body\": True},\n            {\"type\": \"http.response.body\", \"body\": b\"\", \"more_body\": False},\n        ]\n\n        # Verify sequence\n        assert messages[0][\"type\"] == \"http.response.start\"\n        assert all(m[\"more_body\"] for m in messages[1:-1])\n        assert messages[-1][\"more_body\"] is False\n\n    def test_valid_sequence_with_informational(self):\n        \"\"\"Test valid sequence with informational response.\"\"\"\n        messages = [\n            {\n                \"type\": \"http.response.informational\",\n                \"status\": 103,\n                \"headers\": [(b\"link\", b\"</style.css>; rel=preload\")],\n            },\n            {\"type\": \"http.response.start\", \"status\": 200, \"headers\": []},\n            {\"type\": \"http.response.body\", \"body\": b\"Hello\", \"more_body\": False},\n        ]\n\n        # Informational before start is valid\n        assert messages[0][\"type\"] == \"http.response.informational\"\n        assert messages[1][\"type\"] == \"http.response.start\"\n\n\n# ============================================================================\n# Large Response Tests\n# ============================================================================\n\nclass TestLargeResponses:\n    \"\"\"Tests for handling large responses.\"\"\"\n\n    def test_chunk_size_encoding(self):\n        \"\"\"Test chunk size encoding for various sizes.\"\"\"\n        test_cases = [\n            (1, b\"1\\r\\n\"),\n            (10, b\"a\\r\\n\"),\n            (15, b\"f\\r\\n\"),\n            (16, b\"10\\r\\n\"),\n            (255, b\"ff\\r\\n\"),\n            (256, b\"100\\r\\n\"),\n            (1024, b\"400\\r\\n\"),\n            (65535, b\"ffff\\r\\n\"),\n            (1048576, b\"100000\\r\\n\"),  # 1MB\n        ]\n\n        for size, expected in test_cases:\n            chunk_header = f\"{size:x}\\r\\n\".encode(\"latin-1\")\n            assert chunk_header == expected, f\"Failed for size {size}\"\n\n    def test_megabyte_chunk(self):\n        \"\"\"Test encoding 1MB chunk.\"\"\"\n        size = 1024 * 1024  # 1MB\n        body = b\"x\" * size\n\n        chunk = f\"{len(body):x}\\r\\n\".encode(\"latin-1\") + body + b\"\\r\\n\"\n\n        # Verify structure\n        assert chunk.startswith(b\"100000\\r\\n\")  # 1MB in hex\n        assert chunk.endswith(b\"\\r\\n\")\n        # Total size: header (8) + body (1048576) + trailer (2)\n        assert len(chunk) == 8 + 1048576 + 2\n\n\n# ============================================================================\n# Transfer-Encoding Header Tests\n# ============================================================================\n\nclass TestTransferEncodingHeader:\n    \"\"\"Tests for Transfer-Encoding header handling.\"\"\"\n\n    def test_transfer_encoding_chunked(self):\n        \"\"\"Test Transfer-Encoding: chunked header.\"\"\"\n        headers = [(b\"transfer-encoding\", b\"chunked\")]\n\n        te_header = dict(headers).get(b\"transfer-encoding\")\n        assert te_header == b\"chunked\"\n\n    def test_add_transfer_encoding_to_headers(self):\n        \"\"\"Test adding Transfer-Encoding header to response.\"\"\"\n        headers = [\n            (b\"content-type\", b\"text/plain\"),\n        ]\n\n        # Add chunked encoding\n        headers = list(headers) + [(b\"transfer-encoding\", b\"chunked\")]\n\n        header_names = [name for name, _ in headers]\n        assert b\"transfer-encoding\" in header_names\n\n    def test_no_content_length_with_transfer_encoding(self):\n        \"\"\"Test Content-Length should not be present with Transfer-Encoding.\"\"\"\n        # Per HTTP spec, Content-Length must be ignored if Transfer-Encoding present\n        headers = [\n            (b\"content-type\", b\"text/plain\"),\n            (b\"transfer-encoding\", b\"chunked\"),\n        ]\n\n        header_names = [name for name, _ in headers]\n        assert b\"content-length\" not in header_names\n"
  },
  {
    "path": "tests/test_asgi_uwsgi.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nTests for ASGI uWSGI protocol parser.\n\"\"\"\n\nimport pytest\n\nfrom gunicorn.asgi.unreader import AsyncUnreader\nfrom gunicorn.asgi.uwsgi import AsyncUWSGIRequest\nfrom gunicorn.uwsgi.errors import (\n    InvalidUWSGIHeader,\n    UnsupportedModifier,\n    ForbiddenUWSGIRequest,\n)\n\n\nclass MockStreamReader:\n    \"\"\"Mock asyncio.StreamReader for testing.\"\"\"\n\n    def __init__(self, data):\n        self.data = data\n        self.pos = 0\n\n    async def read(self, size=-1):\n        if self.pos >= len(self.data):\n            return b\"\"\n        if size < 0:\n            result = self.data[self.pos:]\n            self.pos = len(self.data)\n        else:\n            result = self.data[self.pos:self.pos + size]\n            self.pos += size\n        return result\n\n\nclass MockConfig:\n    \"\"\"Mock gunicorn config for testing.\"\"\"\n\n    def __init__(self):\n        self.is_ssl = False\n        self.uwsgi_allow_ips = ['*']  # Allow all for most tests\n\n\ndef build_uwsgi_packet(vars_dict, modifier1=0, modifier2=0):\n    \"\"\"Build a uWSGI packet from a dictionary of variables.\n\n    Args:\n        vars_dict: Dictionary of uWSGI variables\n        modifier1: uWSGI modifier1 (default 0 for WSGI)\n        modifier2: uWSGI modifier2 (default 0)\n\n    Returns:\n        bytes: Complete uWSGI packet\n    \"\"\"\n    vars_data = b\"\"\n    for key, value in vars_dict.items():\n        key_bytes = key.encode('latin-1')\n        value_bytes = value.encode('latin-1')\n        vars_data += len(key_bytes).to_bytes(2, 'little')\n        vars_data += key_bytes\n        vars_data += len(value_bytes).to_bytes(2, 'little')\n        vars_data += value_bytes\n\n    # Build header: modifier1 (1 byte) + datasize (2 bytes LE) + modifier2 (1 byte)\n    header = bytes([modifier1])\n    header += len(vars_data).to_bytes(2, 'little')\n    header += bytes([modifier2])\n\n    return header + vars_data\n\n\n# Basic parsing tests\n\n@pytest.mark.asyncio\nasync def test_parse_simple_get():\n    \"\"\"Test parsing a simple GET request.\"\"\"\n    vars_dict = {\n        'REQUEST_METHOD': 'GET',\n        'PATH_INFO': '/test',\n        'QUERY_STRING': '',\n        'HTTP_HOST': 'localhost',\n    }\n    packet = build_uwsgi_packet(vars_dict)\n    reader = MockStreamReader(packet)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    request = await AsyncUWSGIRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n\n    assert request.method == \"GET\"\n    assert request.path == \"/test\"\n    assert request.query == \"\"\n    assert request.uri == \"/test\"\n    assert request.version == (1, 1)\n\n\n@pytest.mark.asyncio\nasync def test_parse_get_with_query():\n    \"\"\"Test parsing GET request with query string.\"\"\"\n    vars_dict = {\n        'REQUEST_METHOD': 'GET',\n        'PATH_INFO': '/search',\n        'QUERY_STRING': 'q=test&page=1',\n    }\n    packet = build_uwsgi_packet(vars_dict)\n    reader = MockStreamReader(packet)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    request = await AsyncUWSGIRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n\n    assert request.method == \"GET\"\n    assert request.path == \"/search\"\n    assert request.query == \"q=test&page=1\"\n    assert request.uri == \"/search?q=test&page=1\"\n\n\n@pytest.mark.asyncio\nasync def test_parse_post_with_content_length():\n    \"\"\"Test parsing POST request with content length.\"\"\"\n    body = b\"hello=world\"\n    vars_dict = {\n        'REQUEST_METHOD': 'POST',\n        'PATH_INFO': '/submit',\n        'CONTENT_LENGTH': str(len(body)),\n        'CONTENT_TYPE': 'application/x-www-form-urlencoded',\n    }\n    packet = build_uwsgi_packet(vars_dict) + body\n    reader = MockStreamReader(packet)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    request = await AsyncUWSGIRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n\n    assert request.method == \"POST\"\n    assert request.path == \"/submit\"\n    assert request.content_length == len(body)\n\n    # Read body\n    read_body = await request.read_body(100)\n    assert read_body == body\n\n\n@pytest.mark.asyncio\nasync def test_parse_headers():\n    \"\"\"Test that HTTP headers are correctly extracted.\"\"\"\n    vars_dict = {\n        'REQUEST_METHOD': 'GET',\n        'PATH_INFO': '/',\n        'HTTP_HOST': 'example.com',\n        'HTTP_ACCEPT': 'text/html',\n        'HTTP_X_CUSTOM_HEADER': 'custom-value',\n        'CONTENT_TYPE': 'text/plain',\n        'CONTENT_LENGTH': '0',\n    }\n    packet = build_uwsgi_packet(vars_dict)\n    reader = MockStreamReader(packet)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    request = await AsyncUWSGIRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n\n    # Check headers were extracted correctly\n    assert request.get_header('HOST') == 'example.com'\n    assert request.get_header('ACCEPT') == 'text/html'\n    assert request.get_header('X-CUSTOM-HEADER') == 'custom-value'\n    assert request.get_header('CONTENT-TYPE') == 'text/plain'\n    assert request.get_header('CONTENT-LENGTH') == '0'\n\n\n@pytest.mark.asyncio\nasync def test_parse_https_scheme():\n    \"\"\"Test HTTPS scheme detection.\"\"\"\n    vars_dict = {\n        'REQUEST_METHOD': 'GET',\n        'PATH_INFO': '/',\n        'HTTPS': 'on',\n    }\n    packet = build_uwsgi_packet(vars_dict)\n    reader = MockStreamReader(packet)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    request = await AsyncUWSGIRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n\n    assert request.scheme == 'https'\n\n\n@pytest.mark.asyncio\nasync def test_parse_wsgi_url_scheme():\n    \"\"\"Test wsgi.url_scheme variable.\"\"\"\n    vars_dict = {\n        'REQUEST_METHOD': 'GET',\n        'PATH_INFO': '/',\n        'wsgi.url_scheme': 'https',\n    }\n    packet = build_uwsgi_packet(vars_dict)\n    reader = MockStreamReader(packet)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    request = await AsyncUWSGIRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n\n    assert request.scheme == 'https'\n\n\n# Body reading tests\n\n@pytest.mark.asyncio\nasync def test_read_body_chunks():\n    \"\"\"Test reading body in chunks.\"\"\"\n    body = b\"a\" * 100\n    vars_dict = {\n        'REQUEST_METHOD': 'POST',\n        'PATH_INFO': '/',\n        'CONTENT_LENGTH': str(len(body)),\n    }\n    packet = build_uwsgi_packet(vars_dict) + body\n    reader = MockStreamReader(packet)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    request = await AsyncUWSGIRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n\n    # Read in chunks\n    chunks = []\n    while True:\n        chunk = await request.read_body(30)\n        if not chunk:\n            break\n        chunks.append(chunk)\n\n    assert b\"\".join(chunks) == body\n\n\n@pytest.mark.asyncio\nasync def test_drain_body():\n    \"\"\"Test draining unread body.\"\"\"\n    body = b\"x\" * 50\n    vars_dict = {\n        'REQUEST_METHOD': 'POST',\n        'PATH_INFO': '/',\n        'CONTENT_LENGTH': str(len(body)),\n    }\n    packet = build_uwsgi_packet(vars_dict) + body\n    reader = MockStreamReader(packet)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    request = await AsyncUWSGIRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n\n    # Drain without reading\n    await request.drain_body()\n\n    # Further reads should return empty\n    chunk = await request.read_body()\n    assert chunk == b\"\"\n\n\n@pytest.mark.asyncio\nasync def test_no_body():\n    \"\"\"Test request with no body.\"\"\"\n    vars_dict = {\n        'REQUEST_METHOD': 'GET',\n        'PATH_INFO': '/',\n    }\n    packet = build_uwsgi_packet(vars_dict)\n    reader = MockStreamReader(packet)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    request = await AsyncUWSGIRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n\n    assert request.content_length == 0\n    chunk = await request.read_body()\n    assert chunk == b\"\"\n\n\n# Connection handling tests\n\n@pytest.mark.asyncio\nasync def test_should_close_default():\n    \"\"\"Test default keepalive behavior.\"\"\"\n    vars_dict = {\n        'REQUEST_METHOD': 'GET',\n        'PATH_INFO': '/',\n    }\n    packet = build_uwsgi_packet(vars_dict)\n    reader = MockStreamReader(packet)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    request = await AsyncUWSGIRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n\n    # Default should be keep-alive (HTTP/1.1 behavior)\n    assert request.should_close() is False\n\n\n@pytest.mark.asyncio\nasync def test_should_close_connection_close():\n    \"\"\"Test connection close header.\"\"\"\n    vars_dict = {\n        'REQUEST_METHOD': 'GET',\n        'PATH_INFO': '/',\n        'HTTP_CONNECTION': 'close',\n    }\n    packet = build_uwsgi_packet(vars_dict)\n    reader = MockStreamReader(packet)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    request = await AsyncUWSGIRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n\n    assert request.should_close() is True\n\n\n@pytest.mark.asyncio\nasync def test_should_close_keepalive():\n    \"\"\"Test connection keep-alive header.\"\"\"\n    vars_dict = {\n        'REQUEST_METHOD': 'GET',\n        'PATH_INFO': '/',\n        'HTTP_CONNECTION': 'keep-alive',\n    }\n    packet = build_uwsgi_packet(vars_dict)\n    reader = MockStreamReader(packet)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    request = await AsyncUWSGIRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n\n    assert request.should_close() is False\n\n\n# Error handling tests\n\n@pytest.mark.asyncio\nasync def test_incomplete_header():\n    \"\"\"Test incomplete header raises error.\"\"\"\n    # Only 2 bytes instead of 4\n    data = b\"\\x00\\x00\"\n    reader = MockStreamReader(data)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    with pytest.raises(InvalidUWSGIHeader):\n        await AsyncUWSGIRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n\n\n@pytest.mark.asyncio\nasync def test_unsupported_modifier():\n    \"\"\"Test unsupported modifier1 raises error.\"\"\"\n    # modifier1 = 1 (not WSGI)\n    header = bytes([1, 0, 0, 0])  # modifier1=1, datasize=0, modifier2=0\n    reader = MockStreamReader(header)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    with pytest.raises(UnsupportedModifier):\n        await AsyncUWSGIRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n\n\n@pytest.mark.asyncio\nasync def test_incomplete_vars_block():\n    \"\"\"Test incomplete vars block raises error.\"\"\"\n    # Header says 100 bytes of vars, but only 10 provided\n    header = bytes([0])  # modifier1=0\n    header += (100).to_bytes(2, 'little')  # datasize=100\n    header += bytes([0])  # modifier2=0\n    header += b\"x\" * 10  # Only 10 bytes\n\n    reader = MockStreamReader(header)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    with pytest.raises(InvalidUWSGIHeader):\n        await AsyncUWSGIRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n\n\n@pytest.mark.asyncio\nasync def test_forbidden_ip():\n    \"\"\"Test forbidden IP raises error.\"\"\"\n    vars_dict = {\n        'REQUEST_METHOD': 'GET',\n        'PATH_INFO': '/',\n    }\n    packet = build_uwsgi_packet(vars_dict)\n    reader = MockStreamReader(packet)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n    cfg.uwsgi_allow_ips = ['10.0.0.1']  # Only allow 10.0.0.1\n\n    with pytest.raises(ForbiddenUWSGIRequest):\n        await AsyncUWSGIRequest.parse(cfg, unreader, (\"192.168.1.1\", 8000))\n\n\n@pytest.mark.asyncio\nasync def test_allowed_ip():\n    \"\"\"Test allowed IP succeeds.\"\"\"\n    vars_dict = {\n        'REQUEST_METHOD': 'GET',\n        'PATH_INFO': '/',\n    }\n    packet = build_uwsgi_packet(vars_dict)\n    reader = MockStreamReader(packet)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n    cfg.uwsgi_allow_ips = ['192.168.1.1']\n\n    # Should not raise\n    request = await AsyncUWSGIRequest.parse(cfg, unreader, (\"192.168.1.1\", 8000))\n    assert request.method == \"GET\"\n\n\n@pytest.mark.asyncio\nasync def test_unix_socket_allowed():\n    \"\"\"Test UNIX socket connections are always allowed.\"\"\"\n    vars_dict = {\n        'REQUEST_METHOD': 'GET',\n        'PATH_INFO': '/',\n    }\n    packet = build_uwsgi_packet(vars_dict)\n    reader = MockStreamReader(packet)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n    cfg.uwsgi_allow_ips = ['10.0.0.1']  # Restrictive IP list\n\n    # UNIX socket peer_addr is not a tuple\n    request = await AsyncUWSGIRequest.parse(cfg, unreader, \"/tmp/gunicorn.sock\")\n    assert request.method == \"GET\"\n\n\n# Empty vars block test\n\n@pytest.mark.asyncio\nasync def test_empty_vars_block():\n    \"\"\"Test request with empty vars block uses defaults.\"\"\"\n    # Header with datasize=0\n    header = bytes([0, 0, 0, 0])\n    reader = MockStreamReader(header)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n\n    request = await AsyncUWSGIRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n\n    # Should use defaults\n    assert request.method == \"GET\"\n    assert request.path == \"/\"\n    assert request.query == \"\"\n\n\n# SSL config test\n\n@pytest.mark.asyncio\nasync def test_ssl_config_scheme():\n    \"\"\"Test SSL config sets https scheme.\"\"\"\n    vars_dict = {\n        'REQUEST_METHOD': 'GET',\n        'PATH_INFO': '/',\n    }\n    packet = build_uwsgi_packet(vars_dict)\n    reader = MockStreamReader(packet)\n    unreader = AsyncUnreader(reader)\n    cfg = MockConfig()\n    cfg.is_ssl = True\n\n    request = await AsyncUWSGIRequest.parse(cfg, unreader, (\"127.0.0.1\", 8000))\n\n    assert request.scheme == 'https'\n"
  },
  {
    "path": "tests/test_asgi_websocket_protocol.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nWebSocket RFC 6455 compliance tests.\n\nTests that gunicorn's WebSocket implementation conforms to RFC 6455:\nhttps://tools.ietf.org/html/rfc6455\n\"\"\"\n\nimport asyncio\nimport base64\nimport hashlib\nimport struct\nfrom unittest import mock\n\nimport pytest\n\n\n# ============================================================================\n# WebSocket Constants Tests\n# ============================================================================\n\nclass TestWebSocketConstants:\n    \"\"\"Tests for WebSocket protocol constants.\"\"\"\n\n    def test_websocket_guid(self):\n        \"\"\"Test WebSocket GUID per RFC 6455 Section 1.3.\"\"\"\n        from gunicorn.asgi.websocket import WS_GUID\n\n        # The GUID is a fixed value specified in RFC 6455\n        assert WS_GUID == b\"258EAFA5-E914-47DA-95CA-C5AB0DC85B11\"\n\n    def test_opcode_continuation(self):\n        \"\"\"Test continuation frame opcode (0x0).\"\"\"\n        from gunicorn.asgi.websocket import OPCODE_CONTINUATION\n        assert OPCODE_CONTINUATION == 0x0\n\n    def test_opcode_text(self):\n        \"\"\"Test text frame opcode (0x1).\"\"\"\n        from gunicorn.asgi.websocket import OPCODE_TEXT\n        assert OPCODE_TEXT == 0x1\n\n    def test_opcode_binary(self):\n        \"\"\"Test binary frame opcode (0x2).\"\"\"\n        from gunicorn.asgi.websocket import OPCODE_BINARY\n        assert OPCODE_BINARY == 0x2\n\n    def test_opcode_close(self):\n        \"\"\"Test close frame opcode (0x8).\"\"\"\n        from gunicorn.asgi.websocket import OPCODE_CLOSE\n        assert OPCODE_CLOSE == 0x8\n\n    def test_opcode_ping(self):\n        \"\"\"Test ping frame opcode (0x9).\"\"\"\n        from gunicorn.asgi.websocket import OPCODE_PING\n        assert OPCODE_PING == 0x9\n\n    def test_opcode_pong(self):\n        \"\"\"Test pong frame opcode (0xA).\"\"\"\n        from gunicorn.asgi.websocket import OPCODE_PONG\n        assert OPCODE_PONG == 0xA\n\n\n# ============================================================================\n# WebSocket Close Codes Tests (RFC 6455 Section 7.4.1)\n# ============================================================================\n\nclass TestWebSocketCloseCodes:\n    \"\"\"Tests for WebSocket close status codes.\"\"\"\n\n    def test_close_normal(self):\n        \"\"\"Test normal closure code (1000).\"\"\"\n        from gunicorn.asgi.websocket import CLOSE_NORMAL\n        assert CLOSE_NORMAL == 1000\n\n    def test_close_going_away(self):\n        \"\"\"Test going away code (1001).\"\"\"\n        from gunicorn.asgi.websocket import CLOSE_GOING_AWAY\n        assert CLOSE_GOING_AWAY == 1001\n\n    def test_close_protocol_error(self):\n        \"\"\"Test protocol error code (1002).\"\"\"\n        from gunicorn.asgi.websocket import CLOSE_PROTOCOL_ERROR\n        assert CLOSE_PROTOCOL_ERROR == 1002\n\n    def test_close_unsupported(self):\n        \"\"\"Test unsupported data code (1003).\"\"\"\n        from gunicorn.asgi.websocket import CLOSE_UNSUPPORTED\n        assert CLOSE_UNSUPPORTED == 1003\n\n    def test_close_no_status(self):\n        \"\"\"Test no status received code (1005).\"\"\"\n        from gunicorn.asgi.websocket import CLOSE_NO_STATUS\n        assert CLOSE_NO_STATUS == 1005\n\n    def test_close_abnormal(self):\n        \"\"\"Test abnormal closure code (1006).\"\"\"\n        from gunicorn.asgi.websocket import CLOSE_ABNORMAL\n        assert CLOSE_ABNORMAL == 1006\n\n    def test_close_invalid_data(self):\n        \"\"\"Test invalid frame payload data code (1007).\"\"\"\n        from gunicorn.asgi.websocket import CLOSE_INVALID_DATA\n        assert CLOSE_INVALID_DATA == 1007\n\n    def test_close_policy_violation(self):\n        \"\"\"Test policy violation code (1008).\"\"\"\n        from gunicorn.asgi.websocket import CLOSE_POLICY_VIOLATION\n        assert CLOSE_POLICY_VIOLATION == 1008\n\n    def test_close_message_too_big(self):\n        \"\"\"Test message too big code (1009).\"\"\"\n        from gunicorn.asgi.websocket import CLOSE_MESSAGE_TOO_BIG\n        assert CLOSE_MESSAGE_TOO_BIG == 1009\n\n    def test_close_mandatory_ext(self):\n        \"\"\"Test mandatory extension code (1010).\"\"\"\n        from gunicorn.asgi.websocket import CLOSE_MANDATORY_EXT\n        assert CLOSE_MANDATORY_EXT == 1010\n\n    def test_close_internal_error(self):\n        \"\"\"Test internal server error code (1011).\"\"\"\n        from gunicorn.asgi.websocket import CLOSE_INTERNAL_ERROR\n        assert CLOSE_INTERNAL_ERROR == 1011\n\n\n# ============================================================================\n# WebSocket Handshake Tests (RFC 6455 Section 4.2.2)\n# ============================================================================\n\nclass TestWebSocketHandshake:\n    \"\"\"Tests for WebSocket handshake implementation.\"\"\"\n\n    def test_accept_key_calculation(self):\n        \"\"\"Test Sec-WebSocket-Accept key calculation per RFC 6455.\"\"\"\n        from gunicorn.asgi.websocket import WS_GUID\n\n        # Example from RFC 6455 Section 1.3\n        client_key = b\"dGhlIHNhbXBsZSBub25jZQ==\"\n        expected_accept = \"s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\"\n\n        # Calculation: Base64(SHA-1(client_key + GUID))\n        accept_key = base64.b64encode(\n            hashlib.sha1(client_key + WS_GUID).digest()\n        ).decode(\"ascii\")\n\n        assert accept_key == expected_accept\n\n    def test_accept_key_another_example(self):\n        \"\"\"Test accept key calculation with another key.\"\"\"\n        from gunicorn.asgi.websocket import WS_GUID\n\n        # Another example key\n        client_key = b\"x3JJHMbDL1EzLkh9GBhXDw==\"\n\n        accept_key = base64.b64encode(\n            hashlib.sha1(client_key + WS_GUID).digest()\n        ).decode(\"ascii\")\n\n        # Verify it's a valid base64 string\n        assert len(accept_key) == 28  # SHA-1 hash is 20 bytes, base64 encoded\n        # Verify we can decode it\n        decoded = base64.b64decode(accept_key)\n        assert len(decoded) == 20  # SHA-1 produces 20 bytes\n\n\n# ============================================================================\n# WebSocket Frame Masking Tests (RFC 6455 Section 5.3)\n# ============================================================================\n\nclass TestWebSocketFrameMasking:\n    \"\"\"Tests for WebSocket frame masking/unmasking.\"\"\"\n\n    def _create_protocol(self):\n        \"\"\"Create a WebSocketProtocol instance for testing.\"\"\"\n        from gunicorn.asgi.websocket import WebSocketProtocol\n        return WebSocketProtocol(None, None, {}, None, mock.Mock())\n\n    def test_unmask_simple(self):\n        \"\"\"Test basic unmasking operation.\"\"\"\n        protocol = self._create_protocol()\n\n        # Mask key and masked \"Hello\"\n        masking_key = bytes([0x37, 0xfa, 0x21, 0x3d])\n        # H=0x48, e=0x65, l=0x6c, l=0x6c, o=0x6f\n        # Masked: 0x48^0x37=0x7f, 0x65^0xfa=0x9f, 0x6c^0x21=0x4d, 0x6c^0x3d=0x51, 0x6f^0x37=0x58\n        masked_data = bytes([0x7f, 0x9f, 0x4d, 0x51, 0x58])\n\n        unmasked = protocol._unmask(masked_data, masking_key)\n        assert unmasked == b\"Hello\"\n\n    def test_unmask_empty(self):\n        \"\"\"Test unmasking empty payload.\"\"\"\n        protocol = self._create_protocol()\n\n        masking_key = bytes([0x37, 0xfa, 0x21, 0x3d])\n        unmasked = protocol._unmask(b\"\", masking_key)\n\n        assert unmasked == b\"\"\n\n    def test_unmask_longer_message(self):\n        \"\"\"Test unmasking message longer than mask key.\"\"\"\n        protocol = self._create_protocol()\n\n        # The mask cycles every 4 bytes\n        masking_key = bytes([0x01, 0x02, 0x03, 0x04])\n        message = b\"12345678\"  # 8 bytes\n\n        # Manually mask\n        masked = bytes(b ^ masking_key[i % 4] for i, b in enumerate(message))\n\n        # Unmask should give back original\n        unmasked = protocol._unmask(masked, masking_key)\n        assert unmasked == message\n\n    def test_unmask_binary_data(self):\n        \"\"\"Test unmasking binary data.\"\"\"\n        protocol = self._create_protocol()\n\n        masking_key = bytes([0xAB, 0xCD, 0xEF, 0x01])\n        original = bytes([0x00, 0xFF, 0x80, 0x7F, 0x01])\n\n        # Mask the data\n        masked = bytes(b ^ masking_key[i % 4] for i, b in enumerate(original))\n\n        # Unmask should give back original\n        unmasked = protocol._unmask(masked, masking_key)\n        assert unmasked == original\n\n\n# ============================================================================\n# WebSocket Frame Format Tests (RFC 6455 Section 5.2)\n# ============================================================================\n\nclass TestWebSocketFrameFormat:\n    \"\"\"Tests for WebSocket frame format handling.\"\"\"\n\n    def test_frame_header_structure(self):\n        \"\"\"Test understanding of WebSocket frame header structure.\"\"\"\n        # First byte: FIN(1) + RSV1(1) + RSV2(1) + RSV3(1) + OPCODE(4)\n        # Second byte: MASK(1) + PAYLOAD_LEN(7)\n\n        # Text frame, FIN=1, no RSV bits, opcode=0x1\n        first_byte = 0b10000001  # 0x81\n        assert (first_byte >> 7) & 1 == 1  # FIN\n        assert (first_byte >> 6) & 1 == 0  # RSV1\n        assert (first_byte >> 5) & 1 == 0  # RSV2\n        assert (first_byte >> 4) & 1 == 0  # RSV3\n        assert first_byte & 0x0F == 1  # OPCODE (text)\n\n    def test_payload_length_7bit(self):\n        \"\"\"Test 7-bit payload length encoding (0-125).\"\"\"\n        # Payload length 100\n        second_byte = 0b10000000 | 100  # MASK=1, length=100\n        assert (second_byte >> 7) & 1 == 1  # MASK bit\n        assert second_byte & 0x7F == 100  # Length\n\n    def test_payload_length_16bit(self):\n        \"\"\"Test 16-bit payload length encoding (126 indicator).\"\"\"\n        # Length 126 indicates next 2 bytes contain the length\n        second_byte = 0b10000000 | 126  # MASK=1, length indicator=126\n        assert second_byte & 0x7F == 126\n\n        # Extended length as big-endian 16-bit\n        extended_length = 1000\n        packed = struct.pack(\"!H\", extended_length)\n        assert struct.unpack(\"!H\", packed)[0] == 1000\n\n    def test_payload_length_64bit(self):\n        \"\"\"Test 64-bit payload length encoding (127 indicator).\"\"\"\n        # Length 127 indicates next 8 bytes contain the length\n        second_byte = 0b10000000 | 127  # MASK=1, length indicator=127\n        assert second_byte & 0x7F == 127\n\n        # Extended length as big-endian 64-bit\n        extended_length = 100000\n        packed = struct.pack(\"!Q\", extended_length)\n        assert struct.unpack(\"!Q\", packed)[0] == 100000\n\n\n# ============================================================================\n# WebSocket Protocol Instance Tests\n# ============================================================================\n\nclass TestWebSocketProtocolInstance:\n    \"\"\"Tests for WebSocketProtocol instance state.\"\"\"\n\n    def _create_protocol(self, scope=None):\n        \"\"\"Create a WebSocketProtocol instance.\"\"\"\n        from gunicorn.asgi.websocket import WebSocketProtocol\n\n        if scope is None:\n            scope = {\n                \"type\": \"websocket\",\n                \"headers\": [],\n            }\n\n        return WebSocketProtocol(\n            transport=mock.Mock(),\n            reader=mock.Mock(),\n            scope=scope,\n            app=mock.AsyncMock(),\n            log=mock.Mock(),\n        )\n\n    def test_initial_state(self):\n        \"\"\"Test initial protocol state.\"\"\"\n        protocol = self._create_protocol()\n\n        assert protocol.accepted is False\n        assert protocol.closed is False\n        assert protocol.close_code is None\n        assert protocol.close_reason == \"\"\n\n    def test_fragment_state_initial(self):\n        \"\"\"Test initial fragment reassembly state.\"\"\"\n        protocol = self._create_protocol()\n\n        assert protocol._fragments == []\n        assert protocol._fragment_opcode is None\n\n\n# ============================================================================\n# WebSocket ASGI Message Format Tests\n# ============================================================================\n\nclass TestWebSocketASGIMessages:\n    \"\"\"Tests for WebSocket ASGI message formats.\"\"\"\n\n    def test_websocket_connect_message(self):\n        \"\"\"Test websocket.connect message format.\"\"\"\n        message = {\"type\": \"websocket.connect\"}\n        assert message[\"type\"] == \"websocket.connect\"\n\n    def test_websocket_accept_message(self):\n        \"\"\"Test websocket.accept message format.\"\"\"\n        message = {\n            \"type\": \"websocket.accept\",\n            \"subprotocol\": \"graphql-ws\",\n            \"headers\": [\n                (b\"x-custom-header\", b\"value\"),\n            ],\n        }\n\n        assert message[\"type\"] == \"websocket.accept\"\n        assert message[\"subprotocol\"] == \"graphql-ws\"\n\n    def test_websocket_accept_minimal(self):\n        \"\"\"Test minimal websocket.accept message.\"\"\"\n        message = {\"type\": \"websocket.accept\"}\n        assert message[\"type\"] == \"websocket.accept\"\n\n    def test_websocket_receive_text_message(self):\n        \"\"\"Test websocket.receive message with text.\"\"\"\n        message = {\n            \"type\": \"websocket.receive\",\n            \"text\": \"Hello, WebSocket!\",\n        }\n\n        assert message[\"type\"] == \"websocket.receive\"\n        assert \"text\" in message\n        assert isinstance(message[\"text\"], str)\n\n    def test_websocket_receive_binary_message(self):\n        \"\"\"Test websocket.receive message with binary data.\"\"\"\n        message = {\n            \"type\": \"websocket.receive\",\n            \"bytes\": b\"\\x00\\x01\\x02\\x03\",\n        }\n\n        assert message[\"type\"] == \"websocket.receive\"\n        assert \"bytes\" in message\n        assert isinstance(message[\"bytes\"], bytes)\n\n    def test_websocket_send_text_message(self):\n        \"\"\"Test websocket.send message with text.\"\"\"\n        message = {\n            \"type\": \"websocket.send\",\n            \"text\": \"Response text\",\n        }\n\n        assert message[\"type\"] == \"websocket.send\"\n        assert message[\"text\"] == \"Response text\"\n\n    def test_websocket_send_binary_message(self):\n        \"\"\"Test websocket.send message with binary.\"\"\"\n        message = {\n            \"type\": \"websocket.send\",\n            \"bytes\": b\"\\xFF\\xFE\\xFD\",\n        }\n\n        assert message[\"type\"] == \"websocket.send\"\n        assert message[\"bytes\"] == b\"\\xFF\\xFE\\xFD\"\n\n    def test_websocket_disconnect_message(self):\n        \"\"\"Test websocket.disconnect message format.\"\"\"\n        message = {\n            \"type\": \"websocket.disconnect\",\n            \"code\": 1000,\n        }\n\n        assert message[\"type\"] == \"websocket.disconnect\"\n        assert message[\"code\"] == 1000\n\n    def test_websocket_close_message(self):\n        \"\"\"Test websocket.close message format.\"\"\"\n        message = {\n            \"type\": \"websocket.close\",\n            \"code\": 1000,\n            \"reason\": \"Normal closure\",\n        }\n\n        assert message[\"type\"] == \"websocket.close\"\n        assert message[\"code\"] == 1000\n        assert message[\"reason\"] == \"Normal closure\"\n\n\n# ============================================================================\n# WebSocket Upgrade Detection Tests\n# ============================================================================\n\nclass TestWebSocketUpgradeDetection:\n    \"\"\"Tests for WebSocket upgrade request detection.\"\"\"\n\n    def _create_protocol(self):\n        \"\"\"Create an ASGIProtocol instance for testing.\"\"\"\n        from gunicorn.asgi.protocol import ASGIProtocol\n\n        worker = mock.Mock()\n        from gunicorn.config import Config\n        worker.cfg = Config()\n        worker.log = mock.Mock()\n        worker.asgi = mock.Mock()\n\n        return ASGIProtocol(worker)\n\n    def _create_mock_request(self, method=\"GET\", headers=None):\n        \"\"\"Create a mock HTTP request.\"\"\"\n        request = mock.Mock()\n        request.method = method\n        request.headers = headers or []\n        return request\n\n    def test_valid_websocket_upgrade(self):\n        \"\"\"Test detection of valid WebSocket upgrade request.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(\n            method=\"GET\",\n            headers=[\n                (\"UPGRADE\", \"websocket\"),\n                (\"CONNECTION\", \"upgrade\"),\n            ]\n        )\n\n        assert protocol._is_websocket_upgrade(request) is True\n\n    def test_websocket_upgrade_case_insensitive(self):\n        \"\"\"Test WebSocket upgrade detection is case-insensitive.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(\n            method=\"GET\",\n            headers=[\n                (\"UPGRADE\", \"WebSocket\"),\n                (\"CONNECTION\", \"Upgrade\"),\n            ]\n        )\n\n        assert protocol._is_websocket_upgrade(request) is True\n\n    def test_websocket_upgrade_connection_with_keep_alive(self):\n        \"\"\"Test WebSocket upgrade with Connection: upgrade, keep-alive.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(\n            method=\"GET\",\n            headers=[\n                (\"UPGRADE\", \"websocket\"),\n                (\"CONNECTION\", \"upgrade, keep-alive\"),\n            ]\n        )\n\n        assert protocol._is_websocket_upgrade(request) is True\n\n    def test_not_websocket_wrong_method(self):\n        \"\"\"Test non-GET methods are not WebSocket upgrades.\"\"\"\n        protocol = self._create_protocol()\n\n        for method in [\"POST\", \"PUT\", \"DELETE\", \"PATCH\", \"HEAD\", \"OPTIONS\"]:\n            request = self._create_mock_request(\n                method=method,\n                headers=[\n                    (\"UPGRADE\", \"websocket\"),\n                    (\"CONNECTION\", \"upgrade\"),\n                ]\n            )\n            assert protocol._is_websocket_upgrade(request) is False\n\n    def test_not_websocket_missing_upgrade(self):\n        \"\"\"Test missing Upgrade header.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(\n            method=\"GET\",\n            headers=[\n                (\"CONNECTION\", \"upgrade\"),\n            ]\n        )\n\n        assert protocol._is_websocket_upgrade(request) is False\n\n    def test_not_websocket_missing_connection(self):\n        \"\"\"Test missing Connection header.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(\n            method=\"GET\",\n            headers=[\n                (\"UPGRADE\", \"websocket\"),\n            ]\n        )\n\n        # Result should be falsy (None or False) when Connection header is missing\n        assert not protocol._is_websocket_upgrade(request)\n\n    def test_not_websocket_wrong_upgrade_value(self):\n        \"\"\"Test Upgrade header with wrong value.\"\"\"\n        protocol = self._create_protocol()\n        request = self._create_mock_request(\n            method=\"GET\",\n            headers=[\n                (\"UPGRADE\", \"h2c\"),\n                (\"CONNECTION\", \"upgrade\"),\n            ]\n        )\n\n        assert protocol._is_websocket_upgrade(request) is False\n\n\n# ============================================================================\n# WebSocket Close Frame Tests\n# ============================================================================\n\nclass TestWebSocketCloseFrame:\n    \"\"\"Tests for WebSocket close frame handling.\"\"\"\n\n    def test_close_frame_payload_format(self):\n        \"\"\"Test close frame payload format (code + reason).\"\"\"\n        from gunicorn.asgi.websocket import CLOSE_NORMAL\n\n        code = CLOSE_NORMAL\n        reason = \"Goodbye\"\n\n        # Close frame payload: 2-byte big-endian code + UTF-8 reason\n        payload = struct.pack(\"!H\", code) + reason.encode(\"utf-8\")\n\n        # Parse it back\n        parsed_code = struct.unpack(\"!H\", payload[:2])[0]\n        parsed_reason = payload[2:].decode(\"utf-8\")\n\n        assert parsed_code == 1000\n        assert parsed_reason == \"Goodbye\"\n\n    def test_close_frame_empty_reason(self):\n        \"\"\"Test close frame with empty reason.\"\"\"\n        from gunicorn.asgi.websocket import CLOSE_NORMAL\n\n        payload = struct.pack(\"!H\", CLOSE_NORMAL)\n\n        parsed_code = struct.unpack(\"!H\", payload[:2])[0]\n        parsed_reason = payload[2:].decode(\"utf-8\")\n\n        assert parsed_code == 1000\n        assert parsed_reason == \"\"\n\n    def test_close_frame_max_reason_length(self):\n        \"\"\"Test close frame reason max length (125 - 2 = 123 bytes).\"\"\"\n        from gunicorn.asgi.websocket import CLOSE_NORMAL\n\n        # Control frames have max 125 bytes payload\n        # 2 bytes for code, leaving 123 for reason\n        max_reason = \"x\" * 123\n\n        payload = struct.pack(\"!H\", CLOSE_NORMAL) + max_reason.encode(\"utf-8\")\n\n        assert len(payload) == 125  # Max control frame payload\n\n\n# ============================================================================\n# Async WebSocket Tests\n# ============================================================================\n\nclass TestWebSocketAsync:\n    \"\"\"Async tests for WebSocket protocol.\"\"\"\n\n    def _create_protocol(self, scope=None):\n        \"\"\"Create a WebSocketProtocol instance.\"\"\"\n        from gunicorn.asgi.websocket import WebSocketProtocol\n\n        if scope is None:\n            scope = {\n                \"type\": \"websocket\",\n                \"headers\": [(b\"sec-websocket-key\", b\"dGhlIHNhbXBsZSBub25jZQ==\")],\n            }\n\n        transport = mock.Mock()\n        reader = mock.Mock()\n\n        return WebSocketProtocol(\n            transport=transport,\n            reader=reader,\n            scope=scope,\n            app=mock.AsyncMock(),\n            log=mock.Mock(),\n        )\n\n    @pytest.mark.asyncio\n    async def test_receive_returns_from_queue(self):\n        \"\"\"Test that _receive returns items from queue.\"\"\"\n        protocol = self._create_protocol()\n\n        # Put a message on the queue\n        await protocol._receive_queue.put({\"type\": \"websocket.connect\"})\n\n        # Receive should return it\n        message = await protocol._receive()\n        assert message[\"type\"] == \"websocket.connect\"\n\n    @pytest.mark.asyncio\n    async def test_send_accept_sets_flag(self):\n        \"\"\"Test that sending accept sets the accepted flag.\"\"\"\n        protocol = self._create_protocol()\n\n        # Configure mock transport\n        protocol.transport.write = mock.Mock()\n\n        await protocol._send({\"type\": \"websocket.accept\"})\n\n        assert protocol.accepted is True\n\n    @pytest.mark.asyncio\n    async def test_send_accept_twice_raises(self):\n        \"\"\"Test that accepting twice raises RuntimeError.\"\"\"\n        protocol = self._create_protocol()\n        protocol.transport.write = mock.Mock()\n\n        await protocol._send({\"type\": \"websocket.accept\"})\n\n        with pytest.raises(RuntimeError, match=\"already accepted\"):\n            await protocol._send({\"type\": \"websocket.accept\"})\n\n    @pytest.mark.asyncio\n    async def test_send_before_accept_raises(self):\n        \"\"\"Test that sending data before accept raises RuntimeError.\"\"\"\n        protocol = self._create_protocol()\n\n        with pytest.raises(RuntimeError, match=\"not accepted\"):\n            await protocol._send({\"type\": \"websocket.send\", \"text\": \"hello\"})\n\n    @pytest.mark.asyncio\n    async def test_send_after_close_raises(self):\n        \"\"\"Test that sending after close raises RuntimeError.\"\"\"\n        protocol = self._create_protocol()\n        protocol.transport.write = mock.Mock()\n\n        await protocol._send({\"type\": \"websocket.accept\"})\n        protocol.closed = True\n\n        with pytest.raises(RuntimeError, match=\"closed\"):\n            await protocol._send({\"type\": \"websocket.send\", \"text\": \"hello\"})\n\n    @pytest.mark.asyncio\n    async def test_send_close_sets_flag(self):\n        \"\"\"Test that sending close sets the closed flag.\"\"\"\n        protocol = self._create_protocol()\n        protocol.transport.write = mock.Mock()\n\n        await protocol._send({\"type\": \"websocket.close\", \"code\": 1000})\n\n        assert protocol.closed is True\n"
  },
  {
    "path": "tests/test_asgi_worker.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nTests for the ASGI worker.\n\nIncludes unit tests for worker components and integration tests\nthat actually start the server and make HTTP requests.\n\"\"\"\n\nimport asyncio\nimport errno\nimport os\nimport signal\nimport socket\nimport sys\nimport time\nimport threading\nfrom unittest import mock\n\nimport pytest\n\nfrom gunicorn.config import Config\nfrom gunicorn.workers import gasgi\n\n\n# ============================================================================\n# Mock Classes\n# ============================================================================\n\nclass FakeSocket:\n    \"\"\"Mock socket for testing.\"\"\"\n\n    def __init__(self, data=b''):\n        self.data = data\n        self.closed = False\n        self.blocking = True\n        self._fileno = id(self) % 65536\n\n    def fileno(self):\n        return self._fileno\n\n    def setblocking(self, blocking):\n        self.blocking = blocking\n\n    def recv(self, size):\n        if self.closed:\n            raise OSError(errno.EBADF, \"Bad file descriptor\")\n        result = self.data[:size]\n        self.data = self.data[size:]\n        return result\n\n    def send(self, data):\n        if self.closed:\n            raise OSError(errno.EPIPE, \"Broken pipe\")\n        return len(data)\n\n    def close(self):\n        self.closed = True\n\n    def getsockname(self):\n        return ('127.0.0.1', 8000)\n\n    def getpeername(self):\n        return ('127.0.0.1', 12345)\n\n\nclass FakeApp:\n    \"\"\"Mock ASGI application for testing.\"\"\"\n\n    def __init__(self):\n        self.calls = []\n\n    def wsgi(self):\n        return self.asgi_app\n\n    async def asgi_app(self, scope, receive, send):\n        self.calls.append(scope)\n        if scope[\"type\"] == \"lifespan\":\n            while True:\n                message = await receive()\n                if message[\"type\"] == \"lifespan.startup\":\n                    await send({\"type\": \"lifespan.startup.complete\"})\n                elif message[\"type\"] == \"lifespan.shutdown\":\n                    await send({\"type\": \"lifespan.shutdown.complete\"})\n                    return\n        elif scope[\"type\"] == \"http\":\n            await send({\n                \"type\": \"http.response.start\",\n                \"status\": 200,\n                \"headers\": [(b\"content-type\", b\"text/plain\")],\n            })\n            await send({\n                \"type\": \"http.response.body\",\n                \"body\": b\"Hello from ASGI!\",\n            })\n\n\nclass FakeListener:\n    \"\"\"Mock listener socket.\"\"\"\n\n    def __init__(self):\n        self.sock = FakeSocket()\n\n    def getsockname(self):\n        return ('127.0.0.1', 8000)\n\n    def close(self):\n        self.sock.close()\n\n    def __str__(self):\n        return \"http://127.0.0.1:8000\"\n\n\n# ============================================================================\n# Helper Functions\n# ============================================================================\n\ndef _has_uvloop():\n    \"\"\"Check if uvloop is available.\"\"\"\n    try:\n        import uvloop\n        return True\n    except ImportError:\n        return False\n\n\n# ============================================================================\n# Unit Tests for ASGIWorker\n# ============================================================================\n\nclass TestASGIWorkerInit:\n    \"\"\"Tests for ASGIWorker initialization.\"\"\"\n\n    def create_worker(self, **kwargs):\n        \"\"\"Create a worker for testing.\"\"\"\n        cfg = Config()\n        cfg.set('workers', 1)\n        cfg.set('worker_connections', 1000)\n\n        for key, value in kwargs.items():\n            cfg.set(key, value)\n\n        worker = gasgi.ASGIWorker(\n            age=1,\n            ppid=os.getpid(),\n            sockets=[],\n            app=FakeApp(),\n            timeout=30,\n            cfg=cfg,\n            log=mock.Mock(),\n        )\n        return worker\n\n    def test_worker_init(self):\n        \"\"\"Test worker initialization.\"\"\"\n        worker = self.create_worker()\n\n        assert worker.worker_connections == 1000\n        assert worker.nr_conns == 0\n        assert worker.loop is None\n        assert worker.servers == []\n        assert worker.state == {}\n\n    def test_worker_connections_config(self):\n        \"\"\"Test worker_connections configuration.\"\"\"\n        worker = self.create_worker(worker_connections=500)\n        assert worker.worker_connections == 500\n\n\nclass TestASGIWorkerEventLoop:\n    \"\"\"Tests for event loop setup.\"\"\"\n\n    def create_worker(self, **kwargs):\n        \"\"\"Create a worker for testing.\"\"\"\n        cfg = Config()\n        cfg.set('workers', 1)\n        cfg.set('worker_connections', 1000)\n\n        for key, value in kwargs.items():\n            cfg.set(key, value)\n\n        worker = gasgi.ASGIWorker(\n            age=1,\n            ppid=os.getpid(),\n            sockets=[],\n            app=FakeApp(),\n            timeout=30,\n            cfg=cfg,\n            log=mock.Mock(),\n        )\n        return worker\n\n    def test_setup_asyncio_loop(self):\n        \"\"\"Test asyncio event loop setup.\"\"\"\n        worker = self.create_worker(asgi_loop='asyncio')\n        worker._setup_event_loop()\n\n        assert worker.loop is not None\n        assert isinstance(worker.loop, asyncio.AbstractEventLoop)\n        worker.loop.close()\n\n    def test_setup_auto_loop_falls_back_to_asyncio(self):\n        \"\"\"Test that auto mode uses asyncio when uvloop unavailable.\"\"\"\n        worker = self.create_worker(asgi_loop='auto')\n\n        # Mock uvloop import failure\n        with mock.patch.dict('sys.modules', {'uvloop': None}):\n            worker._setup_event_loop()\n\n        assert worker.loop is not None\n        worker.loop.close()\n\n    @pytest.mark.skipif(\n        not _has_uvloop(),\n        reason=\"uvloop not installed\"\n    )\n    def test_setup_uvloop(self):\n        \"\"\"Test uvloop event loop setup.\"\"\"\n        worker = self.create_worker(asgi_loop='uvloop')\n        worker._setup_event_loop()\n\n        import uvloop\n        assert isinstance(worker.loop, uvloop.Loop)\n        worker.loop.close()\n\n\nclass TestASGIWorkerSignals:\n    \"\"\"Tests for signal handling.\"\"\"\n\n    def create_worker(self):\n        \"\"\"Create a worker for testing.\"\"\"\n        cfg = Config()\n        cfg.set('workers', 1)\n        cfg.set('worker_connections', 1000)\n        cfg.set('graceful_timeout', 5)\n\n        worker = gasgi.ASGIWorker(\n            age=1,\n            ppid=os.getpid(),\n            sockets=[],\n            app=FakeApp(),\n            timeout=30,\n            cfg=cfg,\n            log=mock.Mock(),\n        )\n        worker._setup_event_loop()\n        return worker\n\n    def test_handle_exit_sets_alive_false(self):\n        \"\"\"Test that exit signal sets alive=False.\"\"\"\n        worker = self.create_worker()\n        worker.alive = True\n\n        worker.handle_exit_signal()\n\n        assert worker.alive is False\n        worker.loop.close()\n\n    def test_handle_quit_sets_alive_false(self):\n        \"\"\"Test that quit signal sets alive=False.\"\"\"\n        worker = self.create_worker()\n        worker.alive = True\n\n        # Mock the worker_int callback on the worker's cfg settings\n        with mock.patch.object(worker.cfg.settings['worker_int'], 'get', return_value=lambda w: None):\n            worker.handle_quit_signal()\n\n        assert worker.alive is False\n        worker.loop.close()\n\n\n# ============================================================================\n# Tests for Lifespan Protocol\n# ============================================================================\n\nclass TestLifespanManager:\n    \"\"\"Tests for ASGI lifespan protocol.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_lifespan_startup_complete(self):\n        \"\"\"Test successful lifespan startup.\"\"\"\n        from gunicorn.asgi.lifespan import LifespanManager\n\n        startup_called = False\n        shutdown_called = False\n\n        async def app(scope, receive, send):\n            nonlocal startup_called, shutdown_called\n            assert scope[\"type\"] == \"lifespan\"\n            while True:\n                message = await receive()\n                if message[\"type\"] == \"lifespan.startup\":\n                    startup_called = True\n                    await send({\"type\": \"lifespan.startup.complete\"})\n                elif message[\"type\"] == \"lifespan.shutdown\":\n                    shutdown_called = True\n                    await send({\"type\": \"lifespan.shutdown.complete\"})\n                    return\n\n        manager = LifespanManager(app, mock.Mock())\n        await manager.startup()\n\n        assert startup_called\n        assert manager._startup_complete.is_set()\n        assert not manager._startup_failed\n\n        await manager.shutdown()\n        assert shutdown_called\n\n    @pytest.mark.asyncio\n    async def test_lifespan_startup_failed(self):\n        \"\"\"Test lifespan startup failure.\"\"\"\n        from gunicorn.asgi.lifespan import LifespanManager\n\n        async def app(scope, receive, send):\n            message = await receive()\n            if message[\"type\"] == \"lifespan.startup\":\n                await send({\n                    \"type\": \"lifespan.startup.failed\",\n                    \"message\": \"Database connection failed\"\n                })\n\n        manager = LifespanManager(app, mock.Mock())\n\n        with pytest.raises(RuntimeError, match=\"Database connection failed\"):\n            await manager.startup()\n\n    @pytest.mark.asyncio\n    async def test_lifespan_state_shared(self):\n        \"\"\"Test that lifespan state is shared with app.\"\"\"\n        from gunicorn.asgi.lifespan import LifespanManager\n\n        state = {}\n\n        async def app(scope, receive, send):\n            assert \"state\" in scope\n            scope[\"state\"][\"db\"] = \"connected\"\n            message = await receive()\n            await send({\"type\": \"lifespan.startup.complete\"})\n            message = await receive()\n            await send({\"type\": \"lifespan.shutdown.complete\"})\n\n        manager = LifespanManager(app, mock.Mock(), state)\n        await manager.startup()\n\n        assert state.get(\"db\") == \"connected\"\n\n        await manager.shutdown()\n\n\n# ============================================================================\n# Tests for WebSocket Protocol\n# ============================================================================\n\nclass TestWebSocketProtocol:\n    \"\"\"Tests for WebSocket protocol handling.\"\"\"\n\n    def test_websocket_guid(self):\n        \"\"\"Test WebSocket GUID constant.\"\"\"\n        from gunicorn.asgi.websocket import WS_GUID\n        assert WS_GUID == b\"258EAFA5-E914-47DA-95CA-C5AB0DC85B11\"\n\n    def test_websocket_opcodes(self):\n        \"\"\"Test WebSocket opcode constants.\"\"\"\n        from gunicorn.asgi import websocket\n\n        assert websocket.OPCODE_TEXT == 0x1\n        assert websocket.OPCODE_BINARY == 0x2\n        assert websocket.OPCODE_CLOSE == 0x8\n        assert websocket.OPCODE_PING == 0x9\n        assert websocket.OPCODE_PONG == 0xA\n\n    def test_websocket_accept_key_calculation(self):\n        \"\"\"Test WebSocket accept key calculation per RFC 6455.\"\"\"\n        import base64\n        import hashlib\n        from gunicorn.asgi.websocket import WS_GUID\n\n        # Example from RFC 6455\n        client_key = b\"dGhlIHNhbXBsZSBub25jZQ==\"\n        expected_accept = \"s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\"\n\n        accept_key = base64.b64encode(\n            hashlib.sha1(client_key + WS_GUID).digest()\n        ).decode(\"ascii\")\n\n        assert accept_key == expected_accept\n\n    def test_websocket_frame_masking(self):\n        \"\"\"Test WebSocket frame unmasking.\"\"\"\n        from gunicorn.asgi.websocket import WebSocketProtocol\n\n        # Create a minimal protocol instance\n        protocol = WebSocketProtocol(None, None, {}, None, mock.Mock())\n\n        # Test unmasking (XOR operation)\n        masking_key = bytes([0x37, 0xfa, 0x21, 0x3d])\n        masked_data = bytes([0x7f, 0x9f, 0x4d, 0x51, 0x58])  # \"Hello\" masked\n\n        unmasked = protocol._unmask(masked_data, masking_key)\n        assert unmasked == b\"Hello\"\n\n    def test_websocket_frame_masking_empty(self):\n        \"\"\"Test WebSocket frame unmasking with empty payload.\"\"\"\n        from gunicorn.asgi.websocket import WebSocketProtocol\n\n        protocol = WebSocketProtocol(None, None, {}, None, mock.Mock())\n\n        masking_key = bytes([0x37, 0xfa, 0x21, 0x3d])\n        unmasked = protocol._unmask(b\"\", masking_key)\n        assert unmasked == b\"\"\n\n\n# ============================================================================\n# Integration Tests\n# ============================================================================\n\nclass TestASGIIntegration:\n    \"\"\"Integration tests that start actual servers.\"\"\"\n\n    @pytest.fixture\n    def free_port(self):\n        \"\"\"Get a free port for testing.\"\"\"\n        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:\n            s.bind(('127.0.0.1', 0))\n            return s.getsockname()[1]\n\n    @pytest.mark.asyncio\n    async def test_http_request_response(self, free_port):\n        \"\"\"Test basic HTTP request/response cycle.\"\"\"\n        # Simple ASGI app\n        async def app(scope, receive, send):\n            if scope[\"type\"] == \"http\":\n                await send({\n                    \"type\": \"http.response.start\",\n                    \"status\": 200,\n                    \"headers\": [(b\"content-type\", b\"text/plain\")],\n                })\n                await send({\n                    \"type\": \"http.response.body\",\n                    \"body\": b\"Hello, World!\",\n                })\n\n        # Start server\n        loop = asyncio.get_event_loop()\n        server = await loop.create_server(\n            lambda: _TestProtocol(app),\n            '127.0.0.1',\n            free_port,\n        )\n\n        try:\n            # Use asyncio to make HTTP request\n            reader, writer = await asyncio.open_connection('127.0.0.1', free_port)\n            request = f\"GET / HTTP/1.1\\r\\nHost: 127.0.0.1:{free_port}\\r\\n\\r\\n\"\n            writer.write(request.encode())\n            await writer.drain()\n\n            # Read response\n            response = await reader.read(4096)\n            response_text = response.decode()\n\n            assert \"HTTP/1.1 200\" in response_text\n            assert \"Hello, World!\" in response_text\n\n            writer.close()\n            await writer.wait_closed()\n        finally:\n            server.close()\n            await server.wait_closed()\n\n\nclass _TestProtocol(asyncio.Protocol):\n    \"\"\"Minimal protocol for integration testing.\"\"\"\n\n    def __init__(self, app):\n        self.app = app\n        self.transport = None\n\n    def connection_made(self, transport):\n        self.transport = transport\n\n    def data_received(self, data):\n        # Very simple HTTP parsing for testing\n        asyncio.create_task(self._handle(data))\n\n    async def _handle(self, data):\n        # Parse basic HTTP request\n        lines = data.decode().split('\\r\\n')\n        method, path, _ = lines[0].split(' ')\n\n        scope = {\n            \"type\": \"http\",\n            \"asgi\": {\"version\": \"3.0\"},\n            \"http_version\": \"1.1\",\n            \"method\": method,\n            \"path\": path,\n            \"query_string\": b\"\",\n            \"headers\": [],\n            \"server\": (\"127.0.0.1\", 8000),\n            \"client\": (\"127.0.0.1\", 12345),\n        }\n\n        async def receive():\n            return {\"type\": \"http.request\", \"body\": b\"\", \"more_body\": False}\n\n        async def send(message):\n            if message[\"type\"] == \"http.response.start\":\n                status = message[\"status\"]\n                headers = message.get(\"headers\", [])\n                response = f\"HTTP/1.1 {status} OK\\r\\n\"\n                for name, value in headers:\n                    if isinstance(name, bytes):\n                        name = name.decode()\n                    if isinstance(value, bytes):\n                        value = value.decode()\n                    response += f\"{name}: {value}\\r\\n\"\n                response += \"\\r\\n\"\n                self.transport.write(response.encode())\n            elif message[\"type\"] == \"http.response.body\":\n                body = message.get(\"body\", b\"\")\n                self.transport.write(body)\n                if not message.get(\"more_body\", False):\n                    self.transport.close()\n\n        await self.app(scope, receive, send)\n\n\n# ============================================================================\n# ASGI Protocol Tests\n# ============================================================================\n\nclass TestASGIProtocol:\n    \"\"\"Tests for ASGIProtocol.\"\"\"\n\n    def test_reason_phrases(self):\n        \"\"\"Test HTTP reason phrase lookup.\"\"\"\n        from gunicorn.asgi.protocol import ASGIProtocol\n\n        # Create minimal worker mock\n        worker = mock.Mock()\n        worker.cfg = Config()\n        worker.log = mock.Mock()\n        worker.asgi = mock.Mock()\n\n        protocol = ASGIProtocol(worker)\n\n        assert protocol._get_reason_phrase(200) == \"OK\"\n        assert protocol._get_reason_phrase(404) == \"Not Found\"\n        assert protocol._get_reason_phrase(500) == \"Internal Server Error\"\n        assert protocol._get_reason_phrase(999) == \"Unknown\"\n\n    def test_scope_building(self):\n        \"\"\"Test HTTP scope building.\"\"\"\n        from gunicorn.asgi.protocol import ASGIProtocol\n        from gunicorn.asgi.message import AsyncRequest\n\n        worker = mock.Mock()\n        worker.cfg = Config()\n        worker.cfg.set('root_path', '/api')\n        worker.log = mock.Mock()\n        worker.asgi = mock.Mock()\n\n        protocol = ASGIProtocol(worker)\n\n        # Create mock request\n        request = mock.Mock()\n        request.method = \"GET\"\n        request.path = \"/users\"\n        request.query = \"page=1\"\n        request.version = (1, 1)\n        request.scheme = \"http\"\n        request.headers = [(\"HOST\", \"localhost\"), (\"ACCEPT\", \"text/html\")]\n\n        scope = protocol._build_http_scope(\n            request,\n            (\"127.0.0.1\", 8000),  # sockname\n            (\"127.0.0.1\", 12345),  # peername\n        )\n\n        assert scope[\"type\"] == \"http\"\n        assert scope[\"method\"] == \"GET\"\n        assert scope[\"path\"] == \"/users\"\n        assert scope[\"query_string\"] == b\"page=1\"\n        assert scope[\"root_path\"] == \"/api\"\n        assert scope[\"http_version\"] == \"1.1\"\n\n\n# ============================================================================\n# Config Tests\n# ============================================================================\n\nclass TestASGIConfig:\n    \"\"\"Tests for ASGI configuration options.\"\"\"\n\n    def test_asgi_loop_default(self):\n        \"\"\"Test default asgi_loop value.\"\"\"\n        cfg = Config()\n        assert cfg.asgi_loop == \"auto\"\n\n    def test_asgi_loop_validation(self):\n        \"\"\"Test asgi_loop validation.\"\"\"\n        cfg = Config()\n\n        cfg.set('asgi_loop', 'asyncio')\n        assert cfg.asgi_loop == 'asyncio'\n\n        cfg.set('asgi_loop', 'uvloop')\n        assert cfg.asgi_loop == 'uvloop'\n\n        with pytest.raises(ValueError):\n            cfg.set('asgi_loop', 'invalid')\n\n    def test_asgi_lifespan_default(self):\n        \"\"\"Test default asgi_lifespan value.\"\"\"\n        cfg = Config()\n        assert cfg.asgi_lifespan == \"auto\"\n\n    def test_asgi_lifespan_validation(self):\n        \"\"\"Test asgi_lifespan validation.\"\"\"\n        cfg = Config()\n\n        cfg.set('asgi_lifespan', 'on')\n        assert cfg.asgi_lifespan == 'on'\n\n        cfg.set('asgi_lifespan', 'off')\n        assert cfg.asgi_lifespan == 'off'\n\n        with pytest.raises(ValueError):\n            cfg.set('asgi_lifespan', 'invalid')\n\n    def test_root_path_default(self):\n        \"\"\"Test default root_path value.\"\"\"\n        cfg = Config()\n        assert cfg.root_path == \"\"\n\n    def test_root_path_setting(self):\n        \"\"\"Test root_path configuration.\"\"\"\n        cfg = Config()\n        cfg.set('root_path', '/api/v1')\n        assert cfg.root_path == '/api/v1'\n\n\n# ============================================================================\n# HTTP/2 Priority Tests\n# ============================================================================\n\nclass TestASGIHTTP2Priority:\n    \"\"\"Test HTTP/2 priority in ASGI scope.\"\"\"\n\n    def test_http2_priority_in_scope(self):\n        \"\"\"Test that HTTP/2 priority is added to ASGI scope extensions.\"\"\"\n        from gunicorn.asgi.protocol import ASGIProtocol\n\n        worker = mock.Mock()\n        worker.cfg = Config()\n        worker.log = mock.Mock()\n        worker.asgi = mock.Mock()\n\n        protocol = ASGIProtocol(worker)\n\n        # Create mock HTTP/2 request with priority\n        request = mock.Mock()\n        request.method = \"GET\"\n        request.path = \"/test\"\n        request.query = \"\"\n        request.version = (2, 0)\n        request.scheme = \"https\"\n        request.headers = [(\"HOST\", \"localhost\")]\n        request.priority_weight = 128\n        request.priority_depends_on = 3\n\n        scope = protocol._build_http_scope(\n            request,\n            (\"127.0.0.1\", 8443),\n            (\"127.0.0.1\", 12345),\n        )\n\n        assert \"extensions\" in scope\n        assert \"http.response.priority\" in scope[\"extensions\"]\n        assert scope[\"extensions\"][\"http.response.priority\"][\"weight\"] == 128\n        assert scope[\"extensions\"][\"http.response.priority\"][\"depends_on\"] == 3\n\n    def test_http2_priority_in_http2_scope(self):\n        \"\"\"Test that HTTP/2 priority is in _build_http2_scope.\"\"\"\n        from gunicorn.asgi.protocol import ASGIProtocol\n\n        worker = mock.Mock()\n        worker.cfg = Config()\n        worker.log = mock.Mock()\n        worker.asgi = mock.Mock()\n\n        protocol = ASGIProtocol(worker)\n\n        # Create mock HTTP/2 request with priority\n        request = mock.Mock()\n        request.method = \"POST\"\n        request.path = \"/api/data\"\n        request.query = \"id=1\"\n        request.uri = \"/api/data?id=1\"\n        request.scheme = \"https\"\n        request.headers = [(\"HOST\", \"localhost\"), (\"CONTENT-TYPE\", \"application/json\")]\n        request.priority_weight = 256\n        request.priority_depends_on = 1\n\n        scope = protocol._build_http2_scope(\n            request,\n            (\"127.0.0.1\", 8443),\n            (\"127.0.0.1\", 12345),\n        )\n\n        assert scope[\"http_version\"] == \"2\"\n        assert \"extensions\" in scope\n        assert \"http.response.priority\" in scope[\"extensions\"]\n        assert scope[\"extensions\"][\"http.response.priority\"][\"weight\"] == 256\n        assert scope[\"extensions\"][\"http.response.priority\"][\"depends_on\"] == 1\n\n    def test_no_priority_for_http1_requests(self):\n        \"\"\"Test that HTTP/1.1 requests don't have priority extensions.\"\"\"\n        from gunicorn.asgi.protocol import ASGIProtocol\n\n        worker = mock.Mock()\n        worker.cfg = Config()\n        worker.log = mock.Mock()\n        worker.asgi = mock.Mock()\n\n        protocol = ASGIProtocol(worker)\n\n        # Create mock HTTP/1.1 request (no priority attributes)\n        request = mock.Mock(spec=['method', 'path', 'query', 'version',\n                                   'scheme', 'headers'])\n        request.method = \"GET\"\n        request.path = \"/test\"\n        request.query = \"\"\n        request.version = (1, 1)\n        request.scheme = \"http\"\n        request.headers = [(\"HOST\", \"localhost\")]\n\n        scope = protocol._build_http_scope(\n            request,\n            (\"127.0.0.1\", 8000),\n            (\"127.0.0.1\", 12345),\n        )\n\n        # HTTP/1.1 requests should not have extensions with priority\n        assert \"extensions\" not in scope or \"http.response.priority\" not in scope.get(\"extensions\", {})\n\n\n# ============================================================================\n# HTTP/2 Trailers Tests\n# ============================================================================\n\nclass TestASGIHTTP2Trailers:\n    \"\"\"Test HTTP/2 response trailer support in ASGI.\"\"\"\n\n    def test_http2_trailers_extension_in_scope(self):\n        \"\"\"Test that HTTP/2 scope includes http.response.trailers extension.\"\"\"\n        from gunicorn.asgi.protocol import ASGIProtocol\n\n        worker = mock.Mock()\n        worker.cfg = Config()\n        worker.log = mock.Mock()\n        worker.asgi = mock.Mock()\n\n        protocol = ASGIProtocol(worker)\n\n        # Create mock HTTP/2 request\n        request = mock.Mock()\n        request.method = \"GET\"\n        request.path = \"/api\"\n        request.query = \"\"\n        request.uri = \"/api\"\n        request.scheme = \"https\"\n        request.headers = [(\"HOST\", \"localhost\")]\n        request.priority_weight = 16\n        request.priority_depends_on = 0\n\n        scope = protocol._build_http2_scope(\n            request,\n            (\"127.0.0.1\", 8443),\n            (\"127.0.0.1\", 12345),\n        )\n\n        # HTTP/2 scope should have trailers extension\n        assert \"extensions\" in scope\n        assert \"http.response.trailers\" in scope[\"extensions\"]\n\n    def test_http2_scope_has_both_priority_and_trailers(self):\n        \"\"\"Test that HTTP/2 scope includes both priority and trailers extensions.\"\"\"\n        from gunicorn.asgi.protocol import ASGIProtocol\n\n        worker = mock.Mock()\n        worker.cfg = Config()\n        worker.log = mock.Mock()\n        worker.asgi = mock.Mock()\n\n        protocol = ASGIProtocol(worker)\n\n        request = mock.Mock()\n        request.method = \"POST\"\n        request.path = \"/grpc\"\n        request.query = \"\"\n        request.uri = \"/grpc\"\n        request.scheme = \"https\"\n        request.headers = [(\"HOST\", \"localhost\"), (\"CONTENT-TYPE\", \"application/grpc\")]\n        request.priority_weight = 128\n        request.priority_depends_on = 1\n\n        scope = protocol._build_http2_scope(\n            request,\n            (\"127.0.0.1\", 8443),\n            (\"127.0.0.1\", 54321),\n        )\n\n        extensions = scope.get(\"extensions\", {})\n        assert \"http.response.priority\" in extensions\n        assert \"http.response.trailers\" in extensions\n        assert extensions[\"http.response.priority\"][\"weight\"] == 128\n"
  },
  {
    "path": "tests/test_config.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport os\nimport re\nimport sys\n\nimport pytest\n\nfrom gunicorn import config\nfrom gunicorn.app.base import Application\nfrom gunicorn.app.wsgiapp import WSGIApplication\nfrom gunicorn.errors import ConfigError\nfrom gunicorn.util import load_class\nfrom gunicorn.workers.sync import SyncWorker\nfrom gunicorn import glogging\nfrom gunicorn.instrument import statsd\n\ndirname = os.path.dirname(__file__)\ndef cfg_module():\n    return 'config.test_cfg'\ndef alt_cfg_module():\n    return 'config.test_cfg_alt'\ndef cfg_file():\n    return os.path.join(dirname, \"config\", \"test_cfg.py\")\ndef alt_cfg_file():\n    return os.path.join(dirname, \"config\", \"test_cfg_alt.py\")\ndef cfg_file_with_wsgi_app():\n    return os.path.join(dirname, \"config\", \"test_cfg_with_wsgi_app.py\")\ndef paster_ini():\n    return os.path.join(dirname, \"..\", \"examples\", \"frameworks\", \"pylonstest\", \"nose.ini\")\n\n\nclass AltArgs:\n    def __init__(self, args=None):\n        self.args = args or []\n        self.orig = sys.argv\n\n    def __enter__(self):\n        sys.argv = self.args\n\n    def __exit__(self, exc_type, exc_inst, traceback):\n        sys.argv = self.orig\n\n\nclass NoConfigApp(Application):\n    def __init__(self):\n        super().__init__(\"no_usage\", prog=\"gunicorn_test\")\n\n    def init(self, parser, opts, args):\n        pass\n\n    def load(self):\n        pass\n\n\nclass CustomWorker(SyncWorker):\n    pass\n\n\nclass WSGIApp(WSGIApplication):\n    def __init__(self):\n        super().__init__(\"no_usage\", prog=\"gunicorn_test\")\n\n    def load(self):\n        pass\n\n\ndef test_worker_class():\n\n    c = config.Config()\n    c.set(\"worker_class\", CustomWorker)\n    assert c.worker_class == CustomWorker\n\n    try:\n        assert isinstance(load_class(c.worker_class), object)\n    except AttributeError:\n        pytest.fail(\"'load_class doesn't support type class argument'\")\n\n\ndef test_defaults():\n    c = config.Config()\n    for s in config.KNOWN_SETTINGS:\n        assert c.settings[s.name].validator(s.default) == c.settings[s.name].get()\n\n\ndef test_property_access():\n    c = config.Config()\n    for s in config.KNOWN_SETTINGS:\n        getattr(c, s.name)\n\n    # Class was loaded\n    assert c.worker_class == SyncWorker\n\n    # logger class was loaded\n    assert c.logger_class == glogging.Logger\n\n    # Workers defaults to 1\n    assert c.workers == 1\n    c.set(\"workers\", 3)\n    assert c.workers == 3\n\n    # Address is parsed\n    assert c.address == [(\"127.0.0.1\", 8000)]\n\n    # User and group defaults\n    assert os.geteuid() == c.uid\n    assert os.getegid() == c.gid\n\n    # Proc name\n    assert \"gunicorn\" == c.proc_name\n\n    # Not a config property\n    pytest.raises(AttributeError, getattr, c, \"foo\")\n    # Force to be not an error\n    class Baz:\n        def get(self):\n            return 3.14\n    c.settings[\"foo\"] = Baz()\n    assert c.foo == 3.14\n\n    # Attempt to set a cfg not via c.set\n    pytest.raises(AttributeError, setattr, c, \"proc_name\", \"baz\")\n\n    # No setting for name\n    pytest.raises(AttributeError, c.set, \"baz\", \"bar\")\n\n\ndef test_bool_validation():\n    c = config.Config()\n    assert c.preload_app is False\n    c.set(\"preload_app\", True)\n    assert c.preload_app is True\n    c.set(\"preload_app\", \"true\")\n    assert c.preload_app is True\n    c.set(\"preload_app\", \"false\")\n    assert c.preload_app is False\n    pytest.raises(ValueError, c.set, \"preload_app\", \"zilch\")\n    pytest.raises(TypeError, c.set, \"preload_app\", 4)\n\n\ndef test_pos_int_validation():\n    c = config.Config()\n    assert c.workers == 1\n    c.set(\"workers\", 4)\n    assert c.workers == 4\n    c.set(\"workers\", \"5\")\n    assert c.workers == 5\n    c.set(\"workers\", \"0xFF\")\n    assert c.workers == 255\n    c.set(\"workers\", True)\n    assert c.workers == 1  # Yes. That's right...\n    pytest.raises(ValueError, c.set, \"workers\", -21)\n    pytest.raises(TypeError, c.set, \"workers\", c)\n\n\ndef test_str_validation():\n    c = config.Config()\n    assert c.proc_name == \"gunicorn\"\n    c.set(\"proc_name\", \" foo \")\n    assert c.proc_name == \"foo\"\n    pytest.raises(TypeError, c.set, \"proc_name\", 2)\n\n\ndef test_str_to_addr_list_validation():\n    c = config.Config()\n    # Values remain as strings for backward compatibility\n    assert c.proxy_allow_ips == [\"127.0.0.1\", \"::1\"]\n    assert c.forwarded_allow_ips == [\"127.0.0.1\", \"::1\"]\n    # Single IPs are validated but kept as strings\n    c.set(\"forwarded_allow_ips\", \"127.0.0.1,192.0.2.1\")\n    assert c.forwarded_allow_ips == [\"127.0.0.1\", \"192.0.2.1\"]\n    # CIDR networks are supported and kept as strings\n    c.set(\"forwarded_allow_ips\", \"127.0.0.0/8,192.168.0.0/16\")\n    assert c.forwarded_allow_ips == [\"127.0.0.0/8\", \"192.168.0.0/16\"]\n    # Wildcard is preserved as string\n    c.set(\"forwarded_allow_ips\", \"*\")\n    assert c.forwarded_allow_ips == [\"*\"]\n    c.set(\"forwarded_allow_ips\", \"\")\n    assert c.forwarded_allow_ips == []\n    c.set(\"forwarded_allow_ips\", None)\n    assert c.forwarded_allow_ips == []\n    # demand addresses are specified unambiguously\n    pytest.raises(TypeError, c.set, \"forwarded_allow_ips\", 1)\n    # demand networks are specified unambiguously\n    pytest.raises(ValueError, c.set, \"forwarded_allow_ips\", \"127.0.0\")\n    # detect typos\n    pytest.raises(ValueError, c.set, \"forwarded_allow_ips\", \"::f:\")\n    # dangerous typos such as accidentally permitting half the internet\n    # clearly recognizable - masked bits are not zero\n    pytest.raises(ValueError, c.set, \"forwarded_allow_ips\", \"100.64.0.0/1\")\n\n\ndef test_str_to_list():\n    c = config.Config()\n    assert c.forwarder_headers == [\"SCRIPT_NAME\", \"PATH_INFO\"]\n    c.set(\"forwarder_headers\", \"SCRIPT_NAME,REMOTE_USER\")\n    assert c.forwarder_headers == [\"SCRIPT_NAME\", \"REMOTE_USER\"]\n    c.set(\"forwarder_headers\", \"\")\n    assert c.forwarder_headers == []\n    c.set(\"forwarder_headers\", None)\n    assert c.forwarder_headers == []\n\n\ndef test_callable_validation():\n    c = config.Config()\n    def func(a, b):\n        pass\n    c.set(\"pre_fork\", func)\n    assert c.pre_fork == func\n    pytest.raises(TypeError, c.set, \"pre_fork\", 1)\n    pytest.raises(TypeError, c.set, \"pre_fork\", lambda x: True)\n\n\ndef test_reload_engine_validation():\n    c = config.Config()\n\n    assert c.reload_engine == \"auto\"\n\n    c.set('reload_engine', 'poll')\n    assert c.reload_engine == 'poll'\n\n    pytest.raises(ConfigError, c.set, \"reload_engine\", \"invalid\")\n\n\ndef test_callable_validation_for_string():\n    from os.path import isdir as testfunc\n    assert config.validate_callable(-1)(\"os.path.isdir\") == testfunc\n\n    # invalid values tests\n    pytest.raises(\n        TypeError,\n        config.validate_callable(-1), \"\"\n    )\n    pytest.raises(\n        TypeError,\n        config.validate_callable(-1), \"os.path.not_found_func\"\n    )\n    pytest.raises(\n        TypeError,\n        config.validate_callable(-1), \"notfoundmodule.func\"\n    )\n\n\ndef test_cmd_line():\n    with AltArgs([\"prog_name\", \"-b\", \"blargh\"]):\n        app = NoConfigApp()\n        assert app.cfg.bind == [\"blargh\"]\n    with AltArgs([\"prog_name\", \"-w\", \"3\"]):\n        app = NoConfigApp()\n        assert app.cfg.workers == 3\n    with AltArgs([\"prog_name\", \"--preload\"]):\n        app = NoConfigApp()\n        assert app.cfg.preload_app\n\n\ndef test_cmd_line_invalid_setting(capsys):\n    with AltArgs([\"prog_name\", \"-q\", \"bar\"]):\n        with pytest.raises(SystemExit):\n            NoConfigApp()\n        _, err = capsys.readouterr()\n        assert  \"error: unrecognized arguments: -q\" in err\n\n\ndef test_app_config():\n    with AltArgs():\n        app = NoConfigApp()\n    for s in config.KNOWN_SETTINGS:\n        assert app.cfg.settings[s.name].validator(s.default) == app.cfg.settings[s.name].get()\n\n\ndef test_load_config():\n    with AltArgs([\"prog_name\", \"-c\", cfg_file()]):\n        app = NoConfigApp()\n    assert app.cfg.bind == [\"unix:/tmp/bar/baz\"]\n    assert app.cfg.workers == 3\n    assert app.cfg.proc_name == \"fooey\"\n\n\ndef test_load_config_explicit_file():\n    with AltArgs([\"prog_name\", \"-c\", \"file:%s\" % cfg_file()]):\n        app = NoConfigApp()\n    assert app.cfg.bind == [\"unix:/tmp/bar/baz\"]\n    assert app.cfg.workers == 3\n    assert app.cfg.proc_name == \"fooey\"\n\n\ndef test_load_config_module():\n    with AltArgs([\"prog_name\", \"-c\", \"python:%s\" % cfg_module()]):\n        app = NoConfigApp()\n    assert app.cfg.bind == [\"unix:/tmp/bar/baz\"]\n    assert app.cfg.workers == 3\n    assert app.cfg.proc_name == \"fooey\"\n\n\ndef test_cli_overrides_config():\n    with AltArgs([\"prog_name\", \"-c\", cfg_file(), \"-b\", \"blarney\"]):\n        app = NoConfigApp()\n    assert app.cfg.bind == [\"blarney\"]\n    assert app.cfg.proc_name == \"fooey\"\n\n\ndef test_cli_overrides_config_module():\n    with AltArgs([\"prog_name\", \"-c\", \"python:%s\" % cfg_module(), \"-b\", \"blarney\"]):\n        app = NoConfigApp()\n    assert app.cfg.bind == [\"blarney\"]\n    assert app.cfg.proc_name == \"fooey\"\n\n\n@pytest.fixture\ndef create_config_file(request):\n    default_config = os.path.join(os.path.abspath(os.getcwd()),\n                                                      'gunicorn.conf.py')\n    with open(default_config, 'w+') as default:\n        default.write(\"bind='0.0.0.0:9090'\")\n\n    def fin():\n        os.unlink(default_config)\n    request.addfinalizer(fin)\n\n    return default\n\n\ndef test_default_config_file(create_config_file):\n    assert config.get_default_config_file() == create_config_file.name\n\n    with AltArgs([\"prog_name\"]):\n        app = NoConfigApp()\n    assert app.cfg.bind == [\"0.0.0.0:9090\"]\n\n\ndef test_post_request():\n    c = config.Config()\n\n    def post_request_4(worker, req, environ, resp):\n        return 4\n\n    def post_request_3(worker, req, environ):\n        return 3\n\n    def post_request_2(worker, req):\n        return 2\n\n    c.set(\"post_request\", post_request_4)\n    assert c.post_request(1, 2, 3, 4) == 4\n\n    c.set(\"post_request\", post_request_3)\n    assert c.post_request(1, 2, 3, 4) == 3\n\n    c.set(\"post_request\", post_request_2)\n    assert c.post_request(1, 2, 3, 4) == 2\n\n\ndef test_nworkers_changed():\n    c = config.Config()\n\n    def nworkers_changed_3(server, new_value, old_value):\n        return 3\n\n    c.set(\"nworkers_changed\", nworkers_changed_3)\n    assert c.nworkers_changed(1, 2, 3) == 3\n\n\ndef test_statsd_host():\n    c = config.Config()\n    assert c.statsd_host is None\n    c.set(\"statsd_host\", \"localhost\")\n    assert c.statsd_host == (\"localhost\", 8125)\n    c.set(\"statsd_host\", \"statsd:7777\")\n    assert c.statsd_host == (\"statsd\", 7777)\n    c.set(\"statsd_host\", \"unix:///path/to.sock\")\n    assert c.statsd_host == \"/path/to.sock\"\n    pytest.raises(TypeError, c.set, \"statsd_host\", 666)\n    pytest.raises(TypeError, c.set, \"statsd_host\", \"host:string\")\n\n\ndef test_statsd_host_with_unix_as_hostname():\n    # This is a regression test for major release 20. After this release\n    # we should consider modifying the behavior of util.parse_address to\n    # simplify gunicorn's code\n    c = config.Config()\n    c.set(\"statsd_host\", \"unix:7777\")\n    assert c.statsd_host == (\"unix\", 7777)\n    c.set(\"statsd_host\", \"unix://some.socket\")\n    assert c.statsd_host == \"some.socket\"\n\n\ndef test_statsd_changes_logger():\n    c = config.Config()\n    assert c.logger_class == glogging.Logger\n    c.set('statsd_host', 'localhost:12345')\n    assert c.logger_class == statsd.Statsd\n\n\nclass MyLogger(glogging.Logger):\n    # dummy custom logger class for testing\n    pass\n\n\ndef test_always_use_configured_logger():\n    c = config.Config()\n    c.set('logger_class', __name__ + '.MyLogger')\n    assert c.logger_class == MyLogger\n    c.set('statsd_host', 'localhost:12345')\n    # still uses custom logger over statsd\n    assert c.logger_class == MyLogger\n\n\ndef test_load_enviroment_variables_config(monkeypatch):\n    monkeypatch.setenv(\"GUNICORN_CMD_ARGS\", \"--workers=4\")\n    with AltArgs():\n        app = NoConfigApp()\n    assert app.cfg.workers == 4\n\ndef test_config_file_environment_variable(monkeypatch):\n    monkeypatch.setenv(\"GUNICORN_CMD_ARGS\", \"--config=\" + alt_cfg_file())\n    with AltArgs():\n        app = NoConfigApp()\n    assert app.cfg.proc_name == \"not-fooey\"\n    assert app.cfg.config == alt_cfg_file()\n    with AltArgs([\"prog_name\", \"--config\", cfg_file()]):\n        app = NoConfigApp()\n    assert app.cfg.proc_name == \"fooey\"\n    assert app.cfg.config == cfg_file()\n\ndef test_invalid_enviroment_variables_config(monkeypatch, capsys):\n    monkeypatch.setenv(\"GUNICORN_CMD_ARGS\", \"--foo=bar\")\n    with AltArgs():\n        with pytest.raises(SystemExit):\n            NoConfigApp()\n        _, err = capsys.readouterr()\n        assert  \"error: unrecognized arguments: --foo\" in err\n\n\ndef test_cli_overrides_enviroment_variables_module(monkeypatch):\n    monkeypatch.setenv(\"GUNICORN_CMD_ARGS\", \"--workers=4\")\n    with AltArgs([\"prog_name\", \"-c\", cfg_file(), \"--workers\", \"3\"]):\n        app = NoConfigApp()\n    assert app.cfg.workers == 3\n\n\n@pytest.mark.parametrize(\"options, expected\", [\n    ([\"app:app\"], 'app:app'),\n    ([\"-c\", cfg_file(), \"app:app\"], 'app:app'),\n    ([\"-c\", cfg_file_with_wsgi_app(), \"app:app\"], 'app:app'),\n    ([\"-c\", cfg_file_with_wsgi_app()], 'app1:app1'),\n])\ndef test_wsgi_app_config(options, expected):\n    cmdline = [\"prog_name\"]\n    cmdline.extend(options)\n    with AltArgs(cmdline):\n        app = WSGIApp()\n    assert app.app_uri == expected\n\n\n@pytest.mark.parametrize(\"options\", [\n    ([]),\n    ([\"-c\", cfg_file()]),\n])\ndef test_non_wsgi_app(options, capsys):\n    cmdline = [\"prog_name\"]\n    cmdline.extend(options)\n    with AltArgs(cmdline):\n        with pytest.raises(SystemExit):\n            WSGIApp()\n        _, err = capsys.readouterr()\n        assert  \"Error: No application module specified.\" in err\n\n\n@pytest.mark.parametrize(\"options, expected\", [\n    ([\"myapp:app\"], False),\n    ([\"--reload\", \"myapp:app\"], True),\n    ([\"--reload\", \"--\", \"myapp:app\"], True),\n    ([\"--reload\", \"-w 2\", \"myapp:app\"], True),\n])\ndef test_reload(options, expected):\n    cmdline = [\"prog_name\"]\n    cmdline.extend(options)\n    with AltArgs(cmdline):\n        app = NoConfigApp()\n    assert app.cfg.reload == expected\n\n\n@pytest.mark.parametrize(\"options, expected\", [\n    ([\"--umask\", \"0\", \"myapp:app\"], 0),\n    ([\"--umask\", \"0o0\", \"myapp:app\"], 0),\n    ([\"--umask\", \"0x0\", \"myapp:app\"], 0),\n    ([\"--umask\", \"0xFF\", \"myapp:app\"], 255),\n    ([\"--umask\", \"0022\", \"myapp:app\"], 18),\n])\ndef test_umask_config(options, expected):\n    cmdline = [\"prog_name\"]\n    cmdline.extend(options)\n    with AltArgs(cmdline):\n        app = NoConfigApp()\n    assert app.cfg.umask == expected\n\n\ndef _test_ssl_version(options, expected):\n    cmdline = [\"prog_name\"]\n    cmdline.extend(options)\n    with AltArgs(cmdline):\n        app = NoConfigApp()\n    assert app.cfg.ssl_version == expected\n\n\ndef test_bind_fd():\n    with AltArgs([\"prog_name\", \"-b\", \"fd://42\"]):\n        app = NoConfigApp()\n    assert app.cfg.bind == [\"fd://42\"]\n\n\ndef test_repr():\n    c = config.Config()\n    c.set(\"workers\", 5)\n\n    assert \"with value 5\" in repr(c.settings['workers'])\n\n\ndef test_str():\n    c = config.Config()\n    o = str(c)\n\n    # match the first few lines, some different types, but don't go OTT\n    # to avoid needless test fails with changes\n    OUTPUT_MATCH = {\n        'access_log_format': '%(h)s %(l)s %(u)s %(t)s \"%(r)s\" %(s)s %(b)s \"%(f)s\" \"%(a)s\"',\n        'accesslog': 'None',\n        'backlog': '2048',\n        'bind': \"['127.0.0.1:8000']\",\n        'capture_output': 'False',\n        'child_exit': '<ChildExit.child_exit()>',\n    }\n    for i, line in enumerate(o.splitlines()):\n        m = re.match(r'^(\\w+)\\s+= ', line)\n        assert m, \"Line {} didn't match expected format: {!r}\".format(i, line)\n\n        key = m.group(1)\n        try:\n            s = OUTPUT_MATCH.pop(key)\n        except KeyError:\n            continue\n\n        line_re = r'^{}\\s+= {}$'.format(key, re.escape(s))\n        assert re.match(line_re, line), '{!r} != {!r}'.format(line_re, line)\n\n        if not OUTPUT_MATCH:\n            break\n    else:\n        assert False, 'missing expected setting lines? {}'.format(\n            OUTPUT_MATCH.keys()\n        )\n"
  },
  {
    "path": "tests/test_control_socket_integration.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\"\"\"\nIntegration tests for control socket fork safety.\n\nThese tests verify that the control socket server properly handles fork()\nwith different worker types (sync, gthread, gevent) without causing deadlocks.\n\"\"\"\n\nimport os\nimport signal\nimport socket\nimport subprocess\nimport sys\nimport time\nimport tempfile\n\nimport pytest\n\n\n# Timeout for CI environments\nCI_TIMEOUT = 30\n\n\n# Simple WSGI app\nSIMPLE_APP = '''\ndef application(environ, start_response):\n    \"\"\"Basic hello world response.\"\"\"\n    status = '200 OK'\n    body = b'Hello, World!'\n    headers = [\n        ('Content-Type', 'text/plain'),\n        ('Content-Length', str(len(body))),\n    ]\n    start_response(status, headers)\n    return [body]\n'''\n\n\ndef find_free_port():\n    \"\"\"Find a free port to bind to.\"\"\"\n    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:\n        s.bind(('127.0.0.1', 0))\n        return s.getsockname()[1]\n\n\ndef wait_for_server(host, port, timeout=CI_TIMEOUT):\n    \"\"\"Wait until server is accepting connections.\"\"\"\n    start = time.monotonic()\n    while time.monotonic() - start < timeout:\n        try:\n            with socket.create_connection((host, port), timeout=1):\n                return True\n        except (ConnectionRefusedError, socket.timeout, OSError):\n            time.sleep(0.1)\n    return False\n\n\ndef wait_for_socket(socket_path, timeout=CI_TIMEOUT):\n    \"\"\"Wait until Unix socket exists.\"\"\"\n    start = time.monotonic()\n    while time.monotonic() - start < timeout:\n        if os.path.exists(socket_path):\n            return True\n        time.sleep(0.1)\n    return False\n\n\ndef make_request(host, port, path='/'):\n    \"\"\"Make a simple HTTP request and return the response body.\"\"\"\n    with socket.create_connection((host, port), timeout=5) as sock:\n        request = f'GET {path} HTTP/1.1\\r\\nHost: {host}\\r\\nConnection: close\\r\\n\\r\\n'\n        sock.sendall(request.encode())\n        response = b''\n        while True:\n            chunk = sock.recv(4096)\n            if not chunk:\n                break\n            response += chunk\n        return response\n\n\n@pytest.fixture\ndef app_module(tmp_path):\n    \"\"\"Create a temporary app module.\"\"\"\n    app_file = tmp_path / \"app.py\"\n    app_file.write_text(SIMPLE_APP)\n    return str(app_file.parent), \"app:application\"\n\n\ndef start_gunicorn(app_dir, app_name, worker_class, port, control_socket_path):\n    \"\"\"Start a gunicorn server with specified worker class and control socket.\"\"\"\n    cmd = [\n        sys.executable, '-m', 'gunicorn',\n        '--bind', f'127.0.0.1:{port}',\n        '--workers', '2',\n        '--worker-class', worker_class,\n        '--access-logfile', '-',\n        '--error-logfile', '-',\n        '--log-level', 'debug',\n        '--timeout', '30',\n        '--control-socket', control_socket_path,\n        app_name\n    ]\n\n    # Add threads for gthread worker\n    if worker_class == 'gthread':\n        cmd.extend(['--threads', '2'])\n\n    proc = subprocess.Popen(\n        cmd,\n        cwd=app_dir,\n        stdout=subprocess.PIPE,\n        stderr=subprocess.PIPE,\n        env={**os.environ, 'PYTHONPATH': app_dir},\n        preexec_fn=os.setsid\n    )\n\n    return proc\n\n\ndef cleanup_gunicorn(proc):\n    \"\"\"Clean up a gunicorn process.\"\"\"\n    if proc.poll() is None:\n        try:\n            os.killpg(os.getpgid(proc.pid), signal.SIGTERM)\n        except (ProcessLookupError, OSError):\n            pass\n        try:\n            proc.wait(timeout=5)\n        except subprocess.TimeoutExpired:\n            try:\n                os.killpg(os.getpgid(proc.pid), signal.SIGKILL)\n            except (ProcessLookupError, OSError):\n                pass\n            proc.wait()\n\n\ndef get_short_socket_path(prefix):\n    \"\"\"Get a short socket path that won't exceed Unix socket path limits.\n\n    macOS limits Unix socket paths to ~104 characters, so we use /tmp directly.\n    \"\"\"\n    import uuid\n    return f\"/tmp/gunicorn-{prefix}-{uuid.uuid4().hex[:8]}.ctl\"\n\n\nclass TestControlSocketForkSafetySyncWorker:\n    \"\"\"Test control socket fork safety with sync worker.\"\"\"\n\n    def test_sync_worker_boots_with_control_socket(self, app_module, tmp_path):\n        \"\"\"Verify sync worker boots without deadlock when control socket is enabled.\"\"\"\n        app_dir, app_name = app_module\n        port = find_free_port()\n        # Use short path to avoid Unix socket path length limits (104 chars on macOS)\n        control_socket = get_short_socket_path(\"sync\")\n\n        proc = start_gunicorn(app_dir, app_name, 'sync', port, control_socket)\n\n        try:\n            # Wait for server to start - should not deadlock\n            if not wait_for_server('127.0.0.1', port, timeout=15):\n                stdout, stderr = proc.communicate(timeout=1)\n                pytest.fail(\n                    f\"Sync worker deadlocked during startup:\\n\"\n                    f\"stdout: {stdout.decode()}\\n\"\n                    f\"stderr: {stderr.decode()}\"\n                )\n\n            # Verify server responds\n            response = make_request('127.0.0.1', port)\n            assert b'Hello, World!' in response\n\n            # Wait for control socket to be created (started after workers spawn)\n            assert wait_for_socket(control_socket, timeout=5), \\\n                f\"Control socket was not created at {control_socket}\"\n\n        finally:\n            cleanup_gunicorn(proc)\n            # Clean up socket file\n            if os.path.exists(control_socket):\n                os.unlink(control_socket)\n\n\nclass TestControlSocketForkSafetyGthreadWorker:\n    \"\"\"Test control socket fork safety with gthread worker.\"\"\"\n\n    def test_gthread_worker_boots_with_control_socket(self, app_module, tmp_path):\n        \"\"\"Verify gthread worker boots without deadlock when control socket is enabled.\"\"\"\n        app_dir, app_name = app_module\n        port = find_free_port()\n        control_socket = get_short_socket_path(\"gthread\")\n\n        proc = start_gunicorn(app_dir, app_name, 'gthread', port, control_socket)\n\n        try:\n            # Wait for server to start - should not deadlock\n            if not wait_for_server('127.0.0.1', port, timeout=15):\n                stdout, stderr = proc.communicate(timeout=1)\n                pytest.fail(\n                    f\"Gthread worker deadlocked during startup:\\n\"\n                    f\"stdout: {stdout.decode()}\\n\"\n                    f\"stderr: {stderr.decode()}\"\n                )\n\n            # Verify server responds\n            response = make_request('127.0.0.1', port)\n            assert b'Hello, World!' in response\n\n            # Wait for control socket to be created (started after workers spawn)\n            assert wait_for_socket(control_socket, timeout=5), \\\n                f\"Control socket was not created at {control_socket}\"\n\n        finally:\n            cleanup_gunicorn(proc)\n            if os.path.exists(control_socket):\n                os.unlink(control_socket)\n\n\ndef is_gevent_available():\n    \"\"\"Check if gevent is available.\"\"\"\n    try:\n        import gevent  # noqa: F401\n        return True\n    except ImportError:\n        return False\n\n\n@pytest.mark.skipif(not is_gevent_available(), reason=\"gevent not installed\")\nclass TestControlSocketForkSafetyGeventWorker:\n    \"\"\"Test control socket fork safety with gevent worker.\"\"\"\n\n    def test_gevent_worker_boots_with_control_socket(self, app_module, tmp_path):\n        \"\"\"Verify gevent worker boots without deadlock when control socket is enabled.\n\n        This test is critical for issue #3509 - the gevent worker uses monkey\n        patching which can interact badly with asyncio in the control socket thread.\n        \"\"\"\n        app_dir, app_name = app_module\n        port = find_free_port()\n        control_socket = get_short_socket_path(\"gevent\")\n\n        proc = start_gunicorn(app_dir, app_name, 'gevent', port, control_socket)\n\n        try:\n            # Wait for server to start - should not deadlock\n            # Gevent workers may take slightly longer to boot\n            if not wait_for_server('127.0.0.1', port, timeout=20):\n                stdout, stderr = proc.communicate(timeout=1)\n                pytest.fail(\n                    f\"Gevent worker deadlocked during startup:\\n\"\n                    f\"stdout: {stdout.decode()}\\n\"\n                    f\"stderr: {stderr.decode()}\"\n                )\n\n            # Verify server responds\n            response = make_request('127.0.0.1', port)\n            assert b'Hello, World!' in response\n\n            # Wait for control socket to be created (started after workers spawn)\n            assert wait_for_socket(control_socket, timeout=5), \\\n                f\"Control socket was not created at {control_socket}\"\n\n        finally:\n            cleanup_gunicorn(proc)\n            if os.path.exists(control_socket):\n                os.unlink(control_socket)\n\n    def test_gevent_worker_handles_multiple_requests(self, app_module, tmp_path):\n        \"\"\"Verify gevent worker handles multiple requests with control socket enabled.\"\"\"\n        app_dir, app_name = app_module\n        port = find_free_port()\n        control_socket = get_short_socket_path(\"gevent2\")\n\n        proc = start_gunicorn(app_dir, app_name, 'gevent', port, control_socket)\n\n        try:\n            if not wait_for_server('127.0.0.1', port, timeout=20):\n                stdout, stderr = proc.communicate(timeout=1)\n                pytest.fail(\n                    f\"Gevent worker deadlocked during startup:\\n\"\n                    f\"stdout: {stdout.decode()}\\n\"\n                    f\"stderr: {stderr.decode()}\"\n                )\n\n            # Make multiple requests\n            for _ in range(10):\n                response = make_request('127.0.0.1', port)\n                assert b'Hello, World!' in response\n\n            # Verify server is still running\n            assert proc.poll() is None, \"Server died unexpectedly\"\n\n        finally:\n            cleanup_gunicorn(proc)\n            if os.path.exists(control_socket):\n                os.unlink(control_socket)\n\n\nclass TestControlSocketDisabled:\n    \"\"\"Test that disabling control socket works.\"\"\"\n\n    def test_no_control_socket_flag(self, app_module, tmp_path):\n        \"\"\"Verify --no-control-socket flag disables control socket.\"\"\"\n        app_dir, app_name = app_module\n        port = find_free_port()\n        control_socket = str(tmp_path / \"gunicorn.ctl\")\n\n        cmd = [\n            sys.executable, '-m', 'gunicorn',\n            '--bind', f'127.0.0.1:{port}',\n            '--workers', '1',\n            '--worker-class', 'sync',\n            '--access-logfile', '-',\n            '--error-logfile', '-',\n            '--log-level', 'debug',\n            '--no-control-socket',\n            app_name\n        ]\n\n        proc = subprocess.Popen(\n            cmd,\n            cwd=app_dir,\n            stdout=subprocess.PIPE,\n            stderr=subprocess.PIPE,\n            env={**os.environ, 'PYTHONPATH': app_dir},\n            preexec_fn=os.setsid\n        )\n\n        try:\n            if not wait_for_server('127.0.0.1', port, timeout=15):\n                stdout, stderr = proc.communicate(timeout=1)\n                pytest.fail(\n                    f\"Server failed to start:\\n\"\n                    f\"stdout: {stdout.decode()}\\n\"\n                    f\"stderr: {stderr.decode()}\"\n                )\n\n            # Verify server responds\n            response = make_request('127.0.0.1', port)\n            assert b'Hello, World!' in response\n\n            # Verify control socket does NOT exist\n            assert not os.path.exists(control_socket), \"Control socket should not exist\"\n\n        finally:\n            cleanup_gunicorn(proc)\n\n\nclass TestControlSocketAfterReload:\n    \"\"\"Test control socket survives reload.\"\"\"\n\n    def test_control_socket_after_sighup(self, app_module, tmp_path):\n        \"\"\"Verify control socket still works after SIGHUP reload.\"\"\"\n        app_dir, app_name = app_module\n        port = find_free_port()\n        control_socket = get_short_socket_path(\"reload\")\n\n        proc = start_gunicorn(app_dir, app_name, 'sync', port, control_socket)\n\n        try:\n            if not wait_for_server('127.0.0.1', port, timeout=15):\n                stdout, stderr = proc.communicate(timeout=1)\n                pytest.fail(\n                    f\"Server failed to start:\\n\"\n                    f\"stdout: {stdout.decode()}\\n\"\n                    f\"stderr: {stderr.decode()}\"\n                )\n\n            # Verify server and control socket work\n            response = make_request('127.0.0.1', port)\n            assert b'Hello, World!' in response\n            assert wait_for_socket(control_socket, timeout=5), \\\n                f\"Control socket was not created at {control_socket}\"\n\n            # Send SIGHUP to trigger reload\n            proc.send_signal(signal.SIGHUP)\n\n            # Wait for reload to complete\n            time.sleep(2)\n\n            # Verify server still works after reload\n            assert proc.poll() is None, \"Server died after SIGHUP\"\n            response = make_request('127.0.0.1', port)\n            assert b'Hello, World!' in response\n\n            # Verify control socket still exists\n            assert os.path.exists(control_socket), \"Control socket disappeared after reload\"\n\n        finally:\n            cleanup_gunicorn(proc)\n            if os.path.exists(control_socket):\n                os.unlink(control_socket)\n\n\nif __name__ == '__main__':\n    pytest.main([__file__, '-v'])\n"
  },
  {
    "path": "tests/test_dirty_app.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Tests for dirty app module.\"\"\"\n\nimport pytest\n\nfrom gunicorn.dirty.app import (\n    DirtyApp,\n    load_dirty_app,\n    load_dirty_apps,\n    parse_dirty_app_spec,\n)\nfrom gunicorn.dirty.errors import DirtyAppError, DirtyAppNotFoundError\n\n\nclass TestDirtyAppBase:\n    \"\"\"Tests for DirtyApp base class.\"\"\"\n\n    def test_base_class_methods_exist(self):\n        \"\"\"Test that base class has all required methods.\"\"\"\n        app = DirtyApp()\n        assert hasattr(app, 'init')\n        assert hasattr(app, '__call__')\n        assert hasattr(app, 'close')\n        assert callable(app.init)\n        assert callable(app.close)\n\n    def test_base_init_is_noop(self):\n        \"\"\"Test that base init does nothing.\"\"\"\n        app = DirtyApp()\n        result = app.init()\n        assert result is None\n\n    def test_base_close_is_noop(self):\n        \"\"\"Test that base close does nothing.\"\"\"\n        app = DirtyApp()\n        result = app.close()\n        assert result is None\n\n    def test_base_call_dispatches_to_method(self):\n        \"\"\"Test that base __call__ dispatches to methods.\"\"\"\n        class TestApp(DirtyApp):\n            def my_action(self, x, y):\n                return x + y\n\n        app = TestApp()\n        result = app(\"my_action\", 1, 2)\n        assert result == 3\n\n    def test_base_call_unknown_action(self):\n        \"\"\"Test that __call__ raises for unknown action.\"\"\"\n        app = DirtyApp()\n        with pytest.raises(ValueError) as exc_info:\n            app(\"unknown_action\")\n        assert \"Unknown action\" in str(exc_info.value)\n\n    def test_base_call_private_method_rejected(self):\n        \"\"\"Test that __call__ rejects private methods.\"\"\"\n        class TestApp(DirtyApp):\n            def _private(self):\n                return \"secret\"\n\n        app = TestApp()\n        with pytest.raises(ValueError) as exc_info:\n            app(\"_private\")\n        assert \"Unknown action\" in str(exc_info.value)\n\n\nclass TestLoadDirtyApp:\n    \"\"\"Tests for load_dirty_app function.\"\"\"\n\n    def test_load_valid_app(self):\n        \"\"\"Test loading a valid dirty app.\"\"\"\n        app = load_dirty_app(\"tests.support_dirty_app:TestDirtyApp\")\n        assert app is not None\n        assert hasattr(app, 'init')\n        assert hasattr(app, 'close')\n\n    def test_load_app_instance_not_initialized(self):\n        \"\"\"Test that loaded app is not auto-initialized.\"\"\"\n        app = load_dirty_app(\"tests.support_dirty_app:TestDirtyApp\")\n        assert app.initialized is False\n\n    def test_load_app_init_can_be_called(self):\n        \"\"\"Test that init can be called on loaded app.\"\"\"\n        app = load_dirty_app(\"tests.support_dirty_app:TestDirtyApp\")\n        app.init()\n        assert app.initialized is True\n        assert app.data['init_called'] is True\n\n    def test_load_app_call_works(self):\n        \"\"\"Test that loaded app can be called.\"\"\"\n        app = load_dirty_app(\"tests.support_dirty_app:TestDirtyApp\")\n        result = app(\"compute\", 2, 3, operation=\"add\")\n        assert result == 5\n\n        result = app(\"compute\", 2, 3, operation=\"multiply\")\n        assert result == 6\n\n    def test_load_app_close_works(self):\n        \"\"\"Test that close works on loaded app.\"\"\"\n        app = load_dirty_app(\"tests.support_dirty_app:TestDirtyApp\")\n        app(\"store\", \"key\", \"value\")\n        assert app.data.get(\"key\") == \"value\"\n\n        app.close()\n        assert app.closed is True\n        assert app.data == {}\n\n    def test_load_missing_module(self):\n        \"\"\"Test loading from non-existent module.\"\"\"\n        with pytest.raises(DirtyAppNotFoundError) as exc_info:\n            load_dirty_app(\"nonexistent.module:App\")\n        assert \"not found\" in str(exc_info.value).lower()\n\n    def test_load_missing_class(self):\n        \"\"\"Test loading non-existent class from valid module.\"\"\"\n        with pytest.raises(DirtyAppNotFoundError):\n            load_dirty_app(\"tests.support_dirty_app:NonExistentApp\")\n\n    def test_load_invalid_format_no_colon(self):\n        \"\"\"Test loading with invalid format (no colon).\"\"\"\n        with pytest.raises(DirtyAppError) as exc_info:\n            load_dirty_app(\"tests.support_dirty_app.TestDirtyApp\")\n        assert \"Invalid import path format\" in str(exc_info.value)\n\n    def test_load_not_a_class(self):\n        \"\"\"Test loading something that's not a class.\"\"\"\n        with pytest.raises(DirtyAppError) as exc_info:\n            load_dirty_app(\"tests.support_dirty_app:not_a_class\")\n        assert \"not a class\" in str(exc_info.value).lower()\n\n    def test_load_broken_instantiation(self):\n        \"\"\"Test loading an app that fails during instantiation.\"\"\"\n        with pytest.raises(DirtyAppError) as exc_info:\n            load_dirty_app(\"tests.support_dirty_app:BrokenInstantiationApp\")\n        assert \"Failed to instantiate\" in str(exc_info.value)\n\n\nclass TestLoadDirtyApps:\n    \"\"\"Tests for load_dirty_apps function.\"\"\"\n\n    def test_load_multiple_apps(self):\n        \"\"\"Test loading multiple apps.\"\"\"\n        apps = load_dirty_apps([\n            \"tests.support_dirty_app:TestDirtyApp\",\n        ])\n        assert len(apps) == 1\n        assert \"tests.support_dirty_app:TestDirtyApp\" in apps\n\n    def test_load_empty_list(self):\n        \"\"\"Test loading with empty list.\"\"\"\n        apps = load_dirty_apps([])\n        assert apps == {}\n\n    def test_load_multiple_fails_on_first_error(self):\n        \"\"\"Test that loading stops on first error.\"\"\"\n        with pytest.raises(DirtyAppNotFoundError):\n            load_dirty_apps([\n                \"tests.support_dirty_app:TestDirtyApp\",\n                \"nonexistent:App\",  # This should fail\n            ])\n\n\nclass TestDirtyAppStateful:\n    \"\"\"Tests for stateful dirty app behavior.\"\"\"\n\n    def test_app_maintains_state(self):\n        \"\"\"Test that app maintains state between calls.\"\"\"\n        app = load_dirty_app(\"tests.support_dirty_app:TestDirtyApp\")\n        app.init()\n\n        # Store some data\n        app(\"store\", \"model\", {\"weights\": [1, 2, 3]})\n        app(\"store\", \"config\", {\"lr\": 0.001})\n\n        # Retrieve data\n        model = app(\"retrieve\", \"model\")\n        config = app(\"retrieve\", \"config\")\n\n        assert model == {\"weights\": [1, 2, 3]}\n        assert config == {\"lr\": 0.001}\n\n    def test_app_error_handling(self):\n        \"\"\"Test that errors from app are raised properly.\"\"\"\n        app = load_dirty_app(\"tests.support_dirty_app:TestDirtyApp\")\n\n        with pytest.raises(ValueError) as exc_info:\n            app(\"compute\", 1, 2, operation=\"invalid\")\n        assert \"Unknown operation\" in str(exc_info.value)\n\n\nclass TestDirtyAppWorkersAttribute:\n    \"\"\"Tests for DirtyApp workers class attribute.\"\"\"\n\n    def test_default_workers_is_none(self):\n        \"\"\"Base DirtyApp has workers=None (all workers).\"\"\"\n        assert DirtyApp.workers is None\n\n    def test_subclass_can_set_workers(self):\n        \"\"\"Subclass can override workers=2.\"\"\"\n\n        class LimitedApp(DirtyApp):\n            workers = 2\n\n        assert LimitedApp.workers == 2\n\n    def test_workers_inherited_by_default(self):\n        \"\"\"Subclass without workers attr inherits None.\"\"\"\n\n        class InheritedApp(DirtyApp):\n            pass\n\n        assert InheritedApp.workers is None\n\n    def test_instance_has_workers_attribute(self):\n        \"\"\"Instance should have access to workers attribute.\"\"\"\n        app = DirtyApp()\n        assert app.workers is None\n\n        class LimitedApp(DirtyApp):\n            workers = 3\n\n        limited = LimitedApp()\n        assert limited.workers == 3\n\n\nclass TestParseDirtyAppSpec:\n    \"\"\"Tests for parse_dirty_app_spec function.\"\"\"\n\n    def test_standard_format(self):\n        \"\"\"'mod:Class' returns ('mod:Class', None).\"\"\"\n        import_path, count = parse_dirty_app_spec(\"mod:Class\")\n        assert import_path == \"mod:Class\"\n        assert count is None\n\n    def test_standard_format_with_dots(self):\n        \"\"\"'mod.sub.pkg:Class' returns ('mod.sub.pkg:Class', None).\"\"\"\n        import_path, count = parse_dirty_app_spec(\"mod.sub.pkg:Class\")\n        assert import_path == \"mod.sub.pkg:Class\"\n        assert count is None\n\n    def test_with_worker_count(self):\n        \"\"\"'mod:Class:2' returns ('mod:Class', 2).\"\"\"\n        import_path, count = parse_dirty_app_spec(\"mod:Class:2\")\n        assert import_path == \"mod:Class\"\n        assert count == 2\n\n    def test_worker_count_one(self):\n        \"\"\"'mod:Class:1' returns ('mod:Class', 1).\"\"\"\n        import_path, count = parse_dirty_app_spec(\"mod:Class:1\")\n        assert import_path == \"mod:Class\"\n        assert count == 1\n\n    def test_worker_count_large(self):\n        \"\"\"'mod:Class:100' returns ('mod:Class', 100).\"\"\"\n        import_path, count = parse_dirty_app_spec(\"mod:Class:100\")\n        assert import_path == \"mod:Class\"\n        assert count == 100\n\n    def test_worker_count_zero_raises(self):\n        \"\"\"'mod:Class:0' raises DirtyAppError.\"\"\"\n        with pytest.raises(DirtyAppError) as exc_info:\n            parse_dirty_app_spec(\"mod:Class:0\")\n        assert \"must be >= 1\" in str(exc_info.value)\n\n    def test_worker_count_negative_raises(self):\n        \"\"\"'mod:Class:-1' raises DirtyAppError.\"\"\"\n        with pytest.raises(DirtyAppError) as exc_info:\n            parse_dirty_app_spec(\"mod:Class:-1\")\n        assert \"must be >= 1\" in str(exc_info.value)\n\n    def test_non_numeric_raises(self):\n        \"\"\"'mod:Class:abc' raises DirtyAppError.\"\"\"\n        with pytest.raises(DirtyAppError) as exc_info:\n            parse_dirty_app_spec(\"mod:Class:abc\")\n        assert \"Expected integer\" in str(exc_info.value)\n\n    def test_no_colon_raises(self):\n        \"\"\"'mod.Class' (no colon) raises DirtyAppError.\"\"\"\n        with pytest.raises(DirtyAppError) as exc_info:\n            parse_dirty_app_spec(\"mod.Class\")\n        assert \"Invalid import path format\" in str(exc_info.value)\n\n    def test_too_many_colons_raises(self):\n        \"\"\"'mod:Class:2:extra' raises DirtyAppError.\"\"\"\n        with pytest.raises(DirtyAppError) as exc_info:\n            parse_dirty_app_spec(\"mod:Class:2:extra\")\n        assert \"Invalid import path format\" in str(exc_info.value)\n\n    def test_dotted_module_with_count(self):\n        \"\"\"'mod.sub:Class:2' handles dots correctly.\"\"\"\n        import_path, count = parse_dirty_app_spec(\"mod.sub:Class:2\")\n        assert import_path == \"mod.sub:Class\"\n        assert count == 2\n\n\nclass TestGetAppWorkersAttribute:\n    \"\"\"Tests for get_app_workers_attribute function.\"\"\"\n\n    def test_get_workers_none_for_base_class(self):\n        \"\"\"Base DirtyApp returns workers=None.\"\"\"\n        from gunicorn.dirty.app import get_app_workers_attribute\n\n        workers = get_app_workers_attribute(\"gunicorn.dirty.app:DirtyApp\")\n        assert workers is None\n\n    def test_get_workers_from_class_attribute(self):\n        \"\"\"App with workers=2 class attribute returns 2.\"\"\"\n        from gunicorn.dirty.app import get_app_workers_attribute\n\n        workers = get_app_workers_attribute(\"tests.support_dirty_app:HeavyModelApp\")\n        assert workers == 2\n\n    def test_get_workers_none_for_inherited(self):\n        \"\"\"App without explicit workers attribute returns None.\"\"\"\n        from gunicorn.dirty.app import get_app_workers_attribute\n\n        workers = get_app_workers_attribute(\"tests.support_dirty_app:TestDirtyApp\")\n        assert workers is None\n\n    def test_get_workers_not_found_module(self):\n        \"\"\"Non-existent module raises DirtyAppNotFoundError.\"\"\"\n        from gunicorn.dirty.app import get_app_workers_attribute\n        from gunicorn.dirty.errors import DirtyAppNotFoundError\n\n        with pytest.raises(DirtyAppNotFoundError):\n            get_app_workers_attribute(\"nonexistent.module:App\")\n\n    def test_get_workers_not_found_class(self):\n        \"\"\"Non-existent class raises DirtyAppNotFoundError.\"\"\"\n        from gunicorn.dirty.app import get_app_workers_attribute\n        from gunicorn.dirty.errors import DirtyAppNotFoundError\n\n        with pytest.raises(DirtyAppNotFoundError):\n            get_app_workers_attribute(\"tests.support_dirty_app:NonExistentApp\")\n\n    def test_get_workers_invalid_format(self):\n        \"\"\"Invalid format raises DirtyAppError.\"\"\"\n        from gunicorn.dirty.app import get_app_workers_attribute\n        from gunicorn.dirty.errors import DirtyAppError\n\n        with pytest.raises(DirtyAppError) as exc_info:\n            get_app_workers_attribute(\"invalid.format.no.colon\")\n        assert \"Invalid import path format\" in str(exc_info.value)\n"
  },
  {
    "path": "tests/test_dirty_arbiter.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Tests for dirty arbiter module.\"\"\"\n\nimport asyncio\nimport os\nimport signal\nimport struct\nimport tempfile\nimport pytest\n\nfrom gunicorn.config import Config\nfrom gunicorn.dirty.arbiter import DirtyArbiter\nfrom gunicorn.dirty.errors import DirtyError\nfrom gunicorn.dirty.protocol import (\n    DirtyProtocol,\n    BinaryProtocol,\n    make_request,\n    HEADER_SIZE,\n)\n\n\nclass MockStreamWriter:\n    \"\"\"Mock StreamWriter that captures written messages.\"\"\"\n\n    def __init__(self):\n        self.messages = []\n        self._buffer = b\"\"\n        self.closed = False\n\n    def write(self, data):\n        self._buffer += data\n\n    async def drain(self):\n        # Decode the buffer to extract messages using binary protocol\n        while len(self._buffer) >= HEADER_SIZE:\n            # Decode header to get payload length\n            _, _, length = BinaryProtocol.decode_header(\n                self._buffer[:HEADER_SIZE]\n            )\n            total_size = HEADER_SIZE + length\n            if len(self._buffer) >= total_size:\n                msg_data = self._buffer[:total_size]\n                self._buffer = self._buffer[total_size:]\n                # decode_message returns (msg_type_str, request_id, payload_dict)\n                msg_type_str, request_id, payload_dict = BinaryProtocol.decode_message(msg_data)\n                # Reconstruct the dict format for backwards compatibility\n                result = {\"type\": msg_type_str, \"id\": request_id}\n                result.update(payload_dict)\n                self.messages.append(result)\n            else:\n                break\n\n    def close(self):\n        self.closed = True\n\n    async def wait_closed(self):\n        pass\n\n    def get_extra_info(self, name):\n        return None\n\n\nclass MockLog:\n    \"\"\"Mock logger for testing.\"\"\"\n\n    def __init__(self):\n        self.messages = []\n\n    def debug(self, msg, *args):\n        self.messages.append((\"debug\", msg % args if args else msg))\n\n    def info(self, msg, *args):\n        self.messages.append((\"info\", msg % args if args else msg))\n\n    def warning(self, msg, *args):\n        self.messages.append((\"warning\", msg % args if args else msg))\n\n    def error(self, msg, *args):\n        self.messages.append((\"error\", msg % args if args else msg))\n\n    def critical(self, msg, *args):\n        self.messages.append((\"critical\", msg % args if args else msg))\n\n    def exception(self, msg, *args):\n        self.messages.append((\"exception\", msg % args if args else msg))\n\n    def close_on_exec(self):\n        pass\n\n    def reopen_files(self):\n        pass\n\n\nclass TestDirtyArbiterInit:\n    \"\"\"Tests for DirtyArbiter initialization.\"\"\"\n\n    def test_init_attributes(self):\n        \"\"\"Test that arbiter is initialized with correct attributes.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_workers\", 2)\n        cfg.set(\"dirty_apps\", [\"tests.support_dirty_app:TestDirtyApp\"])\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n\n        assert arbiter.cfg == cfg\n        assert arbiter.log == log\n        assert arbiter.workers == {}\n        assert arbiter.alive is True\n        assert arbiter.worker_age == 0\n        assert arbiter.tmpdir is not None\n        assert os.path.isdir(arbiter.tmpdir)\n\n        # Cleanup\n        arbiter._cleanup_sync()\n\n    def test_init_with_custom_socket_path(self):\n        \"\"\"Test initialization with custom socket path.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"custom.sock\")\n            arbiter = DirtyArbiter(cfg=cfg, log=log, socket_path=socket_path)\n\n            assert arbiter.socket_path == socket_path\n\n            # Cleanup\n            arbiter._cleanup_sync()\n\n    def test_init_with_pidfile(self):\n        \"\"\"Test initialization with pidfile parameter.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            pidfile = os.path.join(tmpdir, \"dirty.pid\")\n            arbiter = DirtyArbiter(cfg=cfg, log=log, pidfile=pidfile)\n\n            assert arbiter.pidfile == pidfile\n\n            # Cleanup\n            arbiter._cleanup_sync()\n\n    def test_init_without_pidfile(self):\n        \"\"\"Test initialization without pidfile parameter defaults to None.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n\n        assert arbiter.pidfile is None\n\n        # Cleanup\n        arbiter._cleanup_sync()\n\n\nclass TestDirtyArbiterCleanup:\n    \"\"\"Tests for arbiter cleanup.\"\"\"\n\n    def test_cleanup_removes_socket(self):\n        \"\"\"Test that cleanup removes the socket file.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"test.sock\")\n            arbiter = DirtyArbiter(cfg=cfg, log=log, socket_path=socket_path)\n\n            # Create socket file\n            with open(socket_path, 'w') as f:\n                f.write('')\n\n            assert os.path.exists(socket_path)\n\n            arbiter._cleanup_sync()\n\n            assert not os.path.exists(socket_path)\n\n    def test_cleanup_removes_tmpdir(self):\n        \"\"\"Test that cleanup removes the temp directory.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        tmpdir = arbiter.tmpdir\n\n        assert os.path.isdir(tmpdir)\n\n        arbiter._cleanup_sync()\n\n        assert not os.path.exists(tmpdir)\n\n    def test_cleanup_removes_pidfile(self):\n        \"\"\"Test that cleanup removes the PID file.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            pidfile = os.path.join(tmpdir, \"dirty.pid\")\n            arbiter = DirtyArbiter(cfg=cfg, log=log, pidfile=pidfile)\n\n            # Create pidfile\n            with open(pidfile, 'w') as f:\n                f.write('12345')\n\n            assert os.path.exists(pidfile)\n\n            arbiter._cleanup_sync()\n\n            assert not os.path.exists(pidfile)\n\n    def test_cleanup_handles_missing_pidfile(self):\n        \"\"\"Test that cleanup handles non-existent pidfile gracefully.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            pidfile = os.path.join(tmpdir, \"nonexistent.pid\")\n            arbiter = DirtyArbiter(cfg=cfg, log=log, pidfile=pidfile)\n\n            # Don't create the file\n            assert not os.path.exists(pidfile)\n\n            # Should not raise\n            arbiter._cleanup_sync()\n\n    def test_cleanup_without_pidfile(self):\n        \"\"\"Test that cleanup works when no pidfile configured.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        assert arbiter.pidfile is None\n\n        # Should not raise\n        arbiter._cleanup_sync()\n\n\nclass TestDirtyArbiterPidfileWrite:\n    \"\"\"Tests for PID file writing during run().\"\"\"\n\n    def test_run_writes_pidfile(self):\n        \"\"\"Test that run() writes the PID to the pidfile.\"\"\"\n        from unittest import mock\n\n        cfg = Config()\n        cfg.set(\"dirty_workers\", 0)\n        cfg.set(\"dirty_apps\", [\"tests.support_dirty_app:TestDirtyApp\"])\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            pidfile = os.path.join(tmpdir, \"dirty.pid\")\n            arbiter = DirtyArbiter(cfg=cfg, log=log, pidfile=pidfile)\n\n            # Track if PID file was written correctly\n            pid_written = None\n\n            def mock_asyncio_run(coro):\n                nonlocal pid_written\n                # At this point, PID file should have been written\n                if os.path.exists(pidfile):\n                    with open(pidfile) as f:\n                        pid_written = int(f.read().strip())\n                # Close coroutine to avoid \"never awaited\" warning\n                coro.close()\n\n            # Mock asyncio.run to check PID file before cleanup runs\n            with mock.patch.object(asyncio, 'run', side_effect=mock_asyncio_run):\n                arbiter.run()\n\n            # Check PID was written correctly\n            assert pid_written == os.getpid()\n\n    def test_run_without_pidfile_does_not_fail(self):\n        \"\"\"Test that run() works when no pidfile configured.\"\"\"\n        from unittest import mock\n\n        cfg = Config()\n        cfg.set(\"dirty_workers\", 0)\n        cfg.set(\"dirty_apps\", [\"tests.support_dirty_app:TestDirtyApp\"])\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n\n        def mock_asyncio_run(coro):\n            # Close coroutine to avoid \"never awaited\" warning\n            coro.close()\n\n        with mock.patch.object(asyncio, 'run', side_effect=mock_asyncio_run):\n            # Should not raise\n            arbiter.run()\n\n\nclass TestDirtyArbiterRouteRequest:\n    \"\"\"Tests for request routing.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_route_request_no_workers(self):\n        \"\"\"Test routing request when no workers available.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_workers\", 0)\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.pid = os.getpid()\n\n        request = make_request(\n            request_id=\"test-123\",\n            app_path=\"test:App\",\n            action=\"test\"\n        )\n\n        writer = MockStreamWriter()\n        await arbiter.route_request(request, writer)\n\n        assert len(writer.messages) == 1\n        response = writer.messages[0]\n        assert response[\"type\"] == DirtyProtocol.MSG_TYPE_ERROR\n        assert \"No dirty workers available\" in response[\"error\"][\"message\"]\n\n        arbiter._cleanup_sync()\n\n\nclass TestDirtyArbiterWorkerManagement:\n    \"\"\"Tests for worker management (without actually forking).\"\"\"\n\n    def test_cleanup_worker(self):\n        \"\"\"Test worker cleanup method.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_workers\", 2)\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n\n        # Simulate a worker being registered\n        fake_pid = 99999\n        arbiter.workers[fake_pid] = \"fake_worker\"\n        arbiter.worker_sockets[fake_pid] = \"/tmp/fake.sock\"\n\n        arbiter._cleanup_worker(fake_pid)\n\n        assert fake_pid not in arbiter.workers\n        assert fake_pid not in arbiter.worker_sockets\n\n        arbiter._cleanup_sync()\n\n    @pytest.mark.asyncio\n    async def test_cleanup_worker_cancels_consumer(self):\n        \"\"\"Test that worker cleanup cancels consumer task and removes queue.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_workers\", 2)\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.alive = True\n\n        # Simulate a worker with queue and consumer\n        fake_pid = 99999\n        arbiter.workers[fake_pid] = \"fake_worker\"\n        arbiter.worker_sockets[fake_pid] = \"/tmp/fake.sock\"\n\n        # Create queue and mock consumer task\n        arbiter.worker_queues[fake_pid] = asyncio.Queue()\n\n        async def mock_consumer():\n            try:\n                while True:\n                    await asyncio.sleep(1)\n            except asyncio.CancelledError:\n                pass\n\n        arbiter.worker_consumers[fake_pid] = asyncio.create_task(mock_consumer())\n\n        arbiter._cleanup_worker(fake_pid)\n\n        assert fake_pid not in arbiter.workers\n        assert fake_pid not in arbiter.worker_sockets\n        assert fake_pid not in arbiter.worker_queues\n        assert fake_pid not in arbiter.worker_consumers\n\n        arbiter._cleanup_sync()\n\n    def test_reap_workers_no_children(self):\n        \"\"\"Test reap_workers when no children have exited.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.pid = os.getpid()\n\n        # Should not raise even with no children\n        arbiter.reap_workers()\n\n        arbiter._cleanup_sync()\n\n    def test_close_worker_connection(self):\n        \"\"\"Test _close_worker_connection method.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n\n        # Mock connection\n        class MockWriter:\n            def __init__(self):\n                self.closed = False\n\n            def close(self):\n                self.closed = True\n\n        mock_writer = MockWriter()\n        mock_reader = object()\n        arbiter.worker_connections[99999] = (mock_reader, mock_writer)\n\n        arbiter._close_worker_connection(99999)\n\n        assert 99999 not in arbiter.worker_connections\n        assert mock_writer.closed is True\n\n        arbiter._cleanup_sync()\n\n    def test_close_worker_connection_not_exists(self):\n        \"\"\"Test _close_worker_connection when connection doesn't exist.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n\n        # Should not raise\n        arbiter._close_worker_connection(99999)\n\n        arbiter._cleanup_sync()\n\n\nclass TestDirtyArbiterSignals:\n    \"\"\"Tests for signal handling.\"\"\"\n\n    def test_signal_handler_sigterm(self):\n        \"\"\"Test SIGTERM handling.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.pid = os.getpid()\n\n        assert arbiter.alive is True\n        arbiter._signal_handler(signal.SIGTERM, None)\n        assert arbiter.alive is False\n\n        arbiter._cleanup_sync()\n\n    def test_signal_handler_sigquit(self):\n        \"\"\"Test SIGQUIT handling.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.pid = os.getpid()\n\n        assert arbiter.alive is True\n        arbiter._signal_handler(signal.SIGQUIT, None)\n        assert arbiter.alive is False\n\n        arbiter._cleanup_sync()\n\n    def test_signal_handler_sigint(self):\n        \"\"\"Test SIGINT handling.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.pid = os.getpid()\n\n        assert arbiter.alive is True\n        arbiter._signal_handler(signal.SIGINT, None)\n        assert arbiter.alive is False\n\n        arbiter._cleanup_sync()\n\n    def test_signal_handler_sigusr1_reopens_logs(self):\n        \"\"\"Test SIGUSR1 reopens log files.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.pid = os.getpid()\n\n        assert arbiter.alive is True\n        arbiter._signal_handler(signal.SIGUSR1, None)\n        # Should NOT set alive to False\n        assert arbiter.alive is True\n\n        arbiter._cleanup_sync()\n\n    def test_signal_handler_with_loop(self):\n        \"\"\"Test signal handler calls _shutdown with loop.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.pid = os.getpid()\n\n        # Create mock loop\n        loop = asyncio.new_event_loop()\n        arbiter._loop = loop\n        shutdown_called = []\n\n        def mock_call_soon_threadsafe(cb):\n            shutdown_called.append(cb)\n\n        loop.call_soon_threadsafe = mock_call_soon_threadsafe\n\n        arbiter._signal_handler(signal.SIGTERM, None)\n\n        assert arbiter.alive is False\n        assert len(shutdown_called) == 1\n\n        loop.close()\n        arbiter._cleanup_sync()\n\n\nclass TestDirtyArbiterShutdown:\n    \"\"\"Tests for shutdown.\"\"\"\n\n    def test_shutdown_closes_server(self):\n        \"\"\"Test that _shutdown closes the server.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n\n        class MockServer:\n            def __init__(self):\n                self.closed = False\n\n            def close(self):\n                self.closed = True\n\n        arbiter._server = MockServer()\n        arbiter._shutdown()\n        assert arbiter._server.closed is True\n\n        arbiter._cleanup_sync()\n\n    def test_shutdown_without_server(self):\n        \"\"\"Test _shutdown when server is None.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n\n        # Should not raise\n        arbiter._shutdown()\n\n        arbiter._cleanup_sync()\n\n    def test_init_signals(self):\n        \"\"\"Test init_signals sets up signal handlers.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n\n        original = signal.getsignal(signal.SIGTERM)\n        try:\n            arbiter.init_signals()\n            assert signal.getsignal(signal.SIGTERM) == arbiter._signal_handler\n            assert signal.getsignal(signal.SIGQUIT) == arbiter._signal_handler\n            assert signal.getsignal(signal.SIGINT) == arbiter._signal_handler\n            assert signal.getsignal(signal.SIGHUP) == arbiter._signal_handler\n            assert signal.getsignal(signal.SIGUSR1) == arbiter._signal_handler\n            assert signal.getsignal(signal.SIGCHLD) == arbiter._signal_handler\n        finally:\n            signal.signal(signal.SIGTERM, original)\n\n        arbiter._cleanup_sync()\n\n\nclass TestDirtyArbiterRouteTimeout:\n    \"\"\"Tests for request timeout handling.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_route_request_timeout(self):\n        \"\"\"Test that route_request handles timeout correctly.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_workers\", 0)\n        cfg.set(\"dirty_timeout\", 1)  # 1 second timeout\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.pid = os.getpid()\n\n        # Register a fake worker\n        fake_pid = 99999\n        arbiter.workers[fake_pid] = \"fake_worker\"\n        arbiter.worker_sockets[fake_pid] = \"/tmp/nonexistent.sock\"\n\n        request = make_request(\n            request_id=\"timeout-test\",\n            app_path=\"test:App\",\n            action=\"slow_action\"\n        )\n\n        # This should fail because socket doesn't exist\n        writer = MockStreamWriter()\n        await arbiter.route_request(request, writer)\n\n        assert len(writer.messages) == 1\n        response = writer.messages[0]\n        assert response[\"type\"] == DirtyProtocol.MSG_TYPE_ERROR\n        # Either \"Worker communication failed\" or \"Worker socket not ready\"\n        assert \"error\" in response\n\n        arbiter._cleanup_sync()\n\n    @pytest.mark.asyncio\n    async def test_get_available_worker_returns_first(self):\n        \"\"\"Test _get_available_worker returns first worker.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n\n        # No workers\n        result = await arbiter._get_available_worker()\n        assert result is None\n\n        # Add workers\n        arbiter.workers[1001] = \"worker1\"\n        arbiter.workers[1002] = \"worker2\"\n\n        result = await arbiter._get_available_worker()\n        assert result in [1001, 1002]\n\n        arbiter._cleanup_sync()\n\n\nclass TestDirtyArbiterWorkerConnection:\n    \"\"\"Tests for worker connection management.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_get_worker_connection_cached(self):\n        \"\"\"Test that _get_worker_connection returns cached connection.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n\n        # Set up cached connection\n        mock_reader = object()\n        mock_writer = object()\n        arbiter.worker_connections[99999] = (mock_reader, mock_writer)\n        arbiter.worker_sockets[99999] = \"/tmp/test.sock\"\n\n        reader, writer = await arbiter._get_worker_connection(99999)\n\n        assert reader is mock_reader\n        assert writer is mock_writer\n\n        arbiter._cleanup_sync()\n\n    @pytest.mark.asyncio\n    async def test_get_worker_connection_no_socket(self):\n        \"\"\"Test _get_worker_connection fails when no socket path.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.workers[99999] = \"fake_worker\"\n        # No socket path registered\n\n        with pytest.raises(DirtyError) as exc_info:\n            await arbiter._get_worker_connection(99999)\n\n        assert \"No socket for worker\" in str(exc_info.value)\n\n        arbiter._cleanup_sync()\n\n    @pytest.mark.asyncio\n    async def test_get_worker_connection_socket_not_ready(self):\n        \"\"\"Test _get_worker_connection when socket file doesn't exist.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.workers[99999] = \"fake_worker\"\n        arbiter.worker_sockets[99999] = \"/tmp/nonexistent_socket_12345.sock\"\n\n        with pytest.raises(DirtyError) as exc_info:\n            await arbiter._get_worker_connection(99999)\n\n        assert \"Worker socket not ready\" in str(exc_info.value)\n\n        arbiter._cleanup_sync()\n\n\nclass TestDirtyArbiterManageWorkers:\n    \"\"\"Tests for worker pool management.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_manage_workers_zero_target(self):\n        \"\"\"Test manage_workers with zero target workers.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_workers\", 0)\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.pid = os.getpid()\n\n        # Should not spawn any workers\n        await arbiter.manage_workers()\n        assert len(arbiter.workers) == 0\n\n        arbiter._cleanup_sync()\n\n\nclass TestDirtyArbiterKillWorker:\n    \"\"\"Tests for killing workers.\"\"\"\n\n    def test_kill_worker_no_process(self):\n        \"\"\"Test kill_worker when process doesn't exist.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.pid = os.getpid()\n\n        # Register fake worker\n        arbiter.workers[99999] = \"fake_worker\"\n        arbiter.worker_sockets[99999] = \"/tmp/fake.sock\"\n\n        # Kill non-existent process - should cleanup\n        arbiter.kill_worker(99999, signal.SIGTERM)\n\n        # Worker should be cleaned up\n        assert 99999 not in arbiter.workers\n\n        arbiter._cleanup_sync()\n\n\nclass TestDirtyArbiterMurderWorkers:\n    \"\"\"Tests for worker timeout detection.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_murder_workers_no_timeout_config(self):\n        \"\"\"Test murder_workers with no timeout configured.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_timeout\", 0)  # Disabled\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.pid = os.getpid()\n\n        # Should return early without checking\n        await arbiter.murder_workers()\n\n        arbiter._cleanup_sync()\n\n\nclass TestDirtyArbiterStop:\n    \"\"\"Tests for stop functionality.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_stop_graceful(self):\n        \"\"\"Test graceful stop with no workers.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_graceful_timeout\", 1)\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.pid = os.getpid()\n\n        # No workers - should complete quickly\n        await arbiter.stop(graceful=True)\n\n        arbiter._cleanup_sync()\n\n    @pytest.mark.asyncio\n    async def test_stop_not_graceful(self):\n        \"\"\"Test non-graceful stop.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_graceful_timeout\", 1)\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.pid = os.getpid()\n\n        await arbiter.stop(graceful=False)\n\n        arbiter._cleanup_sync()\n\n\nclass TestDirtyArbiterReload:\n    \"\"\"Tests for reload functionality.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_reload_with_no_workers(self):\n        \"\"\"Test reload when no workers exist.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_workers\", 0)\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.pid = os.getpid()\n\n        # Should complete without spawning\n        await arbiter.reload()\n\n        assert len(arbiter.workers) == 0\n\n        arbiter._cleanup_sync()\n\n\nclass TestDirtyArbiterRunAsync:\n    \"\"\"Tests for async run loop.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_run_async_creates_server(self):\n        \"\"\"Test that _run_async creates Unix server.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_workers\", 0)\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"test_arbiter.sock\")\n            arbiter = DirtyArbiter(cfg=cfg, log=log, socket_path=socket_path)\n            arbiter.pid = os.getpid()\n\n            # Run briefly and stop\n            async def run_briefly():\n                arbiter._loop = asyncio.get_running_loop()\n\n                if os.path.exists(socket_path):\n                    os.unlink(socket_path)\n\n                arbiter._server = await asyncio.start_unix_server(\n                    arbiter.handle_client,\n                    path=socket_path\n                )\n                os.chmod(socket_path, 0o600)\n\n                # Verify socket exists\n                assert os.path.exists(socket_path)\n\n                # Shutdown\n                arbiter._server.close()\n                await arbiter._server.wait_closed()\n\n            await run_briefly()\n\n            arbiter._cleanup_sync()\n\n\nclass TestDirtyArbiterHandleClient:\n    \"\"\"Tests for client connection handling.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_handle_client_connection_close(self):\n        \"\"\"Test handle_client when connection closes.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_workers\", 0)\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.pid = os.getpid()\n        arbiter.alive = True\n\n        # Create reader that returns EOF\n        reader = asyncio.StreamReader()\n        reader.feed_eof()\n\n        class MockWriter:\n            def __init__(self):\n                self.closed = False\n\n            def close(self):\n                self.closed = True\n\n            async def wait_closed(self):\n                pass\n\n        writer = MockWriter()\n\n        # Should exit without error when EOF is received\n        await arbiter.handle_client(reader, writer)\n\n        assert writer.closed is True\n\n        arbiter._cleanup_sync()\n\n\nclass TestDirtyArbiterWorkerMonitor:\n    \"\"\"Tests for worker monitoring.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_worker_monitor_loop(self):\n        \"\"\"Test worker monitor runs periodically.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_workers\", 0)\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.pid = os.getpid()\n        arbiter.ppid = os.getppid()  # Match actual parent for ppid check\n        arbiter.alive = True\n\n        monitor_calls = 0\n\n        async def mock_murder_workers():\n            nonlocal monitor_calls\n            monitor_calls += 1\n            if monitor_calls >= 2:\n                arbiter.alive = False\n\n        async def mock_manage_workers():\n            pass\n\n        arbiter.murder_workers = mock_murder_workers\n        arbiter.manage_workers = mock_manage_workers\n\n        # Run monitor briefly\n        await arbiter._worker_monitor()\n\n        assert monitor_calls >= 2\n\n        arbiter._cleanup_sync()\n\n    @pytest.mark.asyncio\n    async def test_worker_monitor_detects_parent_death(self):\n        \"\"\"Test worker monitor exits when parent dies.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_workers\", 0)\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.pid = os.getpid()\n        arbiter.ppid = 99999  # Fake parent PID that doesn't match os.getppid()\n        arbiter.alive = True\n\n        shutdown_called = []\n\n        def mock_shutdown():\n            shutdown_called.append(True)\n\n        arbiter._shutdown = mock_shutdown\n\n        # Run monitor - should detect parent change and exit\n        await arbiter._worker_monitor()\n\n        # Should have detected parent death\n        assert arbiter.alive is False\n        assert len(shutdown_called) == 1\n\n        # Check log message\n        log_messages = [msg for level, msg in log.messages if level == \"warning\"]\n        assert any(\"Parent changed\" in msg for msg in log_messages)\n\n        arbiter._cleanup_sync()\n\n\nclass TestDirtyArbiterHandleSigchld:\n    \"\"\"Tests for SIGCHLD handling.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_handle_sigchld_reaps_workers(self):\n        \"\"\"Test _handle_sigchld calls reap_workers and manage_workers.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_workers\", 0)\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.pid = os.getpid()\n\n        reap_called = []\n        manage_called = []\n\n        def mock_reap():\n            reap_called.append(True)\n\n        async def mock_manage():\n            manage_called.append(True)\n\n        arbiter.reap_workers = mock_reap\n        arbiter.manage_workers = mock_manage\n\n        await arbiter._handle_sigchld()\n\n        assert len(reap_called) == 1\n        assert len(manage_called) == 1\n\n        arbiter._cleanup_sync()\n\n    def test_sigchld_handler_with_loop(self):\n        \"\"\"Test SIGCHLD signal creates task on loop.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.pid = os.getpid()\n\n        loop = asyncio.new_event_loop()\n        arbiter._loop = loop\n        tasks_scheduled = []\n\n        def mock_call_soon_threadsafe(cb):\n            tasks_scheduled.append(cb)\n\n        loop.call_soon_threadsafe = mock_call_soon_threadsafe\n\n        arbiter._signal_handler(signal.SIGCHLD, None)\n\n        assert len(tasks_scheduled) == 1\n\n        loop.close()\n        arbiter._cleanup_sync()\n\n\nclass TestDirtyArbiterSighupHandler:\n    \"\"\"Tests for SIGHUP (reload) handling.\"\"\"\n\n    def test_sighup_handler_with_loop(self):\n        \"\"\"Test SIGHUP signal schedules reload.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.pid = os.getpid()\n\n        loop = asyncio.new_event_loop()\n        arbiter._loop = loop\n        tasks_scheduled = []\n\n        def mock_call_soon_threadsafe(cb):\n            tasks_scheduled.append(cb)\n\n        loop.call_soon_threadsafe = mock_call_soon_threadsafe\n\n        arbiter._signal_handler(signal.SIGHUP, None)\n\n        # Should still be alive (SIGHUP is reload, not shutdown)\n        assert arbiter.alive is True\n        assert len(tasks_scheduled) == 1\n\n        loop.close()\n        arbiter._cleanup_sync()\n\n\nclass TestDirtyArbiterQueueBehavior:\n    \"\"\"Tests for queue-based request routing.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_start_worker_consumer_creates_queue_and_task(self):\n        \"\"\"Test _start_worker_consumer creates queue and task.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.alive = True\n\n        fake_pid = 99999\n\n        await arbiter._start_worker_consumer(fake_pid)\n\n        assert fake_pid in arbiter.worker_queues\n        assert fake_pid in arbiter.worker_consumers\n        assert isinstance(arbiter.worker_queues[fake_pid], asyncio.Queue)\n        assert isinstance(arbiter.worker_consumers[fake_pid], asyncio.Task)\n\n        # Cancel task for cleanup\n        arbiter.worker_consumers[fake_pid].cancel()\n        try:\n            await arbiter.worker_consumers[fake_pid]\n        except asyncio.CancelledError:\n            pass\n\n        arbiter._cleanup_sync()\n\n    @pytest.mark.asyncio\n    async def test_route_request_starts_consumer_on_demand(self):\n        \"\"\"Test route_request starts consumer if not exists.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.pid = os.getpid()\n        arbiter.alive = True\n\n        # Register fake worker\n        fake_pid = 99999\n        arbiter.workers[fake_pid] = \"fake_worker\"\n        arbiter.worker_sockets[fake_pid] = \"/tmp/nonexistent.sock\"\n\n        assert fake_pid not in arbiter.worker_queues\n        assert fake_pid not in arbiter.worker_consumers\n\n        # Make request - should start consumer\n        request = make_request(\n            request_id=\"test-123\",\n            app_path=\"test:App\",\n            action=\"test\"\n        )\n\n        # This will fail (no socket), but consumer should be started\n        writer = MockStreamWriter()\n        await arbiter.route_request(request, writer)\n\n        assert fake_pid in arbiter.worker_queues\n        assert fake_pid in arbiter.worker_consumers\n\n        # Cleanup\n        arbiter.alive = False\n        arbiter.worker_consumers[fake_pid].cancel()\n        try:\n            await arbiter.worker_consumers[fake_pid]\n        except asyncio.CancelledError:\n            pass\n\n        arbiter._cleanup_sync()\n\n    @pytest.mark.asyncio\n    async def test_stop_cancels_all_consumers(self):\n        \"\"\"Test stop() cancels all consumer tasks.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_graceful_timeout\", 1)\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.pid = os.getpid()\n        arbiter.alive = True\n\n        # Create mock consumers\n        async def mock_consumer():\n            try:\n                while True:\n                    await asyncio.sleep(1)\n            except asyncio.CancelledError:\n                pass\n\n        task1 = asyncio.create_task(mock_consumer())\n        task2 = asyncio.create_task(mock_consumer())\n        arbiter.worker_consumers[1] = task1\n        arbiter.worker_consumers[2] = task2\n\n        await arbiter.stop(graceful=True)\n\n        # Allow cancelled tasks to complete\n        await asyncio.sleep(0)\n\n        # All consumers should be done (cancelled and caught)\n        assert task1.done()\n        assert task2.done()\n\n        arbiter._cleanup_sync()\n\n\nclass TestDirtyArbiterAppTracking:\n    \"\"\"Tests for per-app worker tracking.\"\"\"\n\n    def test_parse_app_specs_standard_format(self):\n        \"\"\"All standard format apps have worker_count=None.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_apps\", [\n            \"tests.support_dirty_app:TestDirtyApp\",\n            \"tests.support_dirty_app:SlowDirtyApp\",\n        ])\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n\n        assert len(arbiter.app_specs) == 2\n        assert arbiter.app_specs[\"tests.support_dirty_app:TestDirtyApp\"][\"worker_count\"] is None\n        assert arbiter.app_specs[\"tests.support_dirty_app:SlowDirtyApp\"][\"worker_count\"] is None\n\n        arbiter._cleanup_sync()\n\n    def test_parse_app_specs_with_worker_count(self):\n        \"\"\"Apps with :N have correct worker_count.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_apps\", [\n            \"tests.support_dirty_app:TestDirtyApp\",\n            \"tests.support_dirty_app:SlowDirtyApp:2\",\n        ])\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n\n        assert arbiter.app_specs[\"tests.support_dirty_app:TestDirtyApp\"][\"worker_count\"] is None\n        assert arbiter.app_specs[\"tests.support_dirty_app:SlowDirtyApp\"][\"worker_count\"] == 2\n\n        arbiter._cleanup_sync()\n\n    def test_get_apps_for_new_worker_all_standard(self):\n        \"\"\"All apps returned when all have workers=None.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_apps\", [\n            \"tests.support_dirty_app:TestDirtyApp\",\n            \"tests.support_dirty_app:SlowDirtyApp\",\n        ])\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n\n        apps = arbiter._get_apps_for_new_worker()\n        assert len(apps) == 2\n        assert \"tests.support_dirty_app:TestDirtyApp\" in apps\n        assert \"tests.support_dirty_app:SlowDirtyApp\" in apps\n\n        arbiter._cleanup_sync()\n\n    def test_get_apps_for_new_worker_respects_limit(self):\n        \"\"\"App with workers=2 stops assigning after 2 workers.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_apps\", [\n            \"tests.support_dirty_app:TestDirtyApp\",      # unlimited\n            \"tests.support_dirty_app:SlowDirtyApp:2\",    # limited to 2\n        ])\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n\n        # First worker should get both apps\n        apps1 = arbiter._get_apps_for_new_worker()\n        assert len(apps1) == 2\n        arbiter._register_worker_apps(1001, apps1)\n\n        # Second worker should get both apps\n        apps2 = arbiter._get_apps_for_new_worker()\n        assert len(apps2) == 2\n        arbiter._register_worker_apps(1002, apps2)\n\n        # Third worker should only get unlimited app\n        apps3 = arbiter._get_apps_for_new_worker()\n        assert len(apps3) == 1\n        assert \"tests.support_dirty_app:TestDirtyApp\" in apps3\n        assert \"tests.support_dirty_app:SlowDirtyApp\" not in apps3\n\n        arbiter._cleanup_sync()\n\n    def test_register_worker_apps_updates_both_maps(self):\n        \"\"\"Both app_worker_map and worker_app_map updated.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_apps\", [\"tests.support_dirty_app:TestDirtyApp\"])\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n\n        app_path = \"tests.support_dirty_app:TestDirtyApp\"\n        arbiter._register_worker_apps(1001, [app_path])\n\n        # Check app_worker_map\n        assert 1001 in arbiter.app_worker_map[app_path]\n\n        # Check worker_app_map\n        assert app_path in arbiter.worker_app_map[1001]\n\n        arbiter._cleanup_sync()\n\n    def test_unregister_worker_cleans_both_maps(self):\n        \"\"\"Worker removal updates both maps correctly.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_apps\", [\"tests.support_dirty_app:TestDirtyApp\"])\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n\n        app_path = \"tests.support_dirty_app:TestDirtyApp\"\n        arbiter._register_worker_apps(1001, [app_path])\n\n        # Verify registered\n        assert 1001 in arbiter.app_worker_map[app_path]\n        assert 1001 in arbiter.worker_app_map\n\n        # Unregister\n        arbiter._unregister_worker(1001)\n\n        # Verify cleaned up\n        assert 1001 not in arbiter.app_worker_map[app_path]\n        assert 1001 not in arbiter.worker_app_map\n\n        arbiter._cleanup_sync()\n\n\nclass TestDirtyArbiterSpawnWorkerPerApp:\n    \"\"\"Tests for spawn_worker with per-app allocation.\"\"\"\n\n    def test_cleanup_worker_queues_apps_for_respawn(self):\n        \"\"\"Dead worker's apps added to _pending_respawns.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_apps\", [\"tests.support_dirty_app:TestDirtyApp\"])\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.pid = 12345\n\n        # Simulate worker registration\n        app_path = \"tests.support_dirty_app:TestDirtyApp\"\n        arbiter.workers[1001] = \"fake_worker\"\n        arbiter.worker_sockets[1001] = \"/tmp/fake.sock\"\n        arbiter._register_worker_apps(1001, [app_path])\n\n        # Cleanup should queue apps for respawn\n        assert len(arbiter._pending_respawns) == 0\n        arbiter._cleanup_worker(1001)\n        assert len(arbiter._pending_respawns) == 1\n        assert app_path in arbiter._pending_respawns[0]\n\n        arbiter._cleanup_sync()\n\n    def test_pending_respawns_cleared_after_spawn(self):\n        \"\"\"Pending respawns consumed when spawning new worker.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_apps\", [\"tests.support_dirty_app:TestDirtyApp\"])\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.pid = 12345\n\n        # Add pending respawn\n        app_path = \"tests.support_dirty_app:TestDirtyApp\"\n        arbiter._pending_respawns.append([app_path])\n\n        # Get apps for new worker should use pending first\n        # But since spawn_worker forks, we test the logic directly\n        assert len(arbiter._pending_respawns) == 1\n\n        # When spawn_worker pops from pending_respawns\n        apps = arbiter._pending_respawns.pop(0)\n        assert apps == [app_path]\n        assert len(arbiter._pending_respawns) == 0\n\n        arbiter._cleanup_sync()\n\n\nclass TestDirtyArbiterRoutingPerApp:\n    \"\"\"Tests for app-aware routing.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_get_available_worker_no_filter(self):\n        \"\"\"Without app_path, returns any worker round-robin.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.workers[1001] = \"worker1\"\n        arbiter.workers[1002] = \"worker2\"\n\n        # Should return workers in round-robin\n        w1 = await arbiter._get_available_worker()\n        w2 = await arbiter._get_available_worker()\n\n        assert w1 in [1001, 1002]\n        assert w2 in [1001, 1002]\n        # They should be different (round-robin)\n        if len(arbiter.workers) >= 2:\n            assert w1 != w2 or len(arbiter.workers) == 1\n\n        arbiter._cleanup_sync()\n\n    @pytest.mark.asyncio\n    async def test_get_available_worker_with_app_filter(self):\n        \"\"\"With app_path, returns only workers that have it.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_apps\", [\"tests.support_dirty_app:TestDirtyApp\"])\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.workers[1001] = \"worker1\"\n        arbiter.workers[1002] = \"worker2\"\n\n        # Only register 1001 for the app\n        app_path = \"tests.support_dirty_app:TestDirtyApp\"\n        arbiter._register_worker_apps(1001, [app_path])\n\n        # Should only return 1001\n        worker = await arbiter._get_available_worker(app_path)\n        assert worker == 1001\n\n        arbiter._cleanup_sync()\n\n    @pytest.mark.asyncio\n    async def test_get_available_worker_app_no_workers_returns_none(self):\n        \"\"\"Returns None if no workers have the app.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_apps\", [\"tests.support_dirty_app:TestDirtyApp\"])\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.workers[1001] = \"worker1\"\n\n        # Worker 1001 has no apps registered - request for unknown app returns None\n        worker = await arbiter._get_available_worker(\"unknown:App\")\n        assert worker is None\n\n        arbiter._cleanup_sync()\n\n    @pytest.mark.asyncio\n    async def test_route_request_app_not_loaded_error(self):\n        \"\"\"Error response when no worker has the app.\"\"\"\n        from gunicorn.dirty.protocol import DirtyProtocol\n\n        cfg = Config()\n        cfg.set(\"dirty_apps\", [\"tests.support_dirty_app:TestDirtyApp\"])\n        log = MockLog()\n\n        arbiter = DirtyArbiter(cfg=cfg, log=log)\n        arbiter.pid = 12345\n        arbiter.workers[1001] = \"worker1\"\n        # No apps registered for this worker (worker exists but has no apps)\n\n        request = make_request(\n            request_id=\"test-123\",\n            app_path=\"unknown:App\",\n            action=\"test\"\n        )\n\n        writer = MockStreamWriter()\n        await arbiter.route_request(request, writer)\n\n        assert len(writer.messages) == 1\n        response = writer.messages[0]\n        assert response[\"type\"] == DirtyProtocol.MSG_TYPE_ERROR\n        assert \"No workers available for app\" in response[\"error\"][\"message\"]\n        assert response[\"error\"][\"error_type\"] == \"DirtyNoWorkersAvailableError\"\n\n        arbiter._cleanup_sync()\n"
  },
  {
    "path": "tests/test_dirty_client.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Tests for dirty client module.\"\"\"\n\nimport os\nimport socket\nimport tempfile\nimport threading\nimport pytest\n\nfrom gunicorn.dirty.client import (\n    DirtyClient,\n    get_dirty_client,\n    get_dirty_socket_path,\n    set_dirty_socket_path,\n    close_dirty_client,\n)\nfrom gunicorn.dirty.errors import DirtyConnectionError, DirtyError\nfrom gunicorn.dirty.protocol import DirtyProtocol, make_response\n\n\nclass TestDirtyClientInit:\n    \"\"\"Tests for DirtyClient initialization.\"\"\"\n\n    def test_init_attributes(self):\n        \"\"\"Test that client is initialized with correct attributes.\"\"\"\n        client = DirtyClient(\"/tmp/test.sock\", timeout=60.0)\n\n        assert client.socket_path == \"/tmp/test.sock\"\n        assert client.timeout == 60.0\n        assert client._sock is None\n        assert client._reader is None\n        assert client._writer is None\n\n\nclass TestDirtyClientSync:\n    \"\"\"Tests for sync API.\"\"\"\n\n    def test_connect_nonexistent_socket(self):\n        \"\"\"Test connecting to non-existent socket.\"\"\"\n        client = DirtyClient(\"/nonexistent/socket.sock\")\n\n        with pytest.raises(DirtyConnectionError) as exc_info:\n            client.connect()\n\n        assert \"Failed to connect\" in str(exc_info.value)\n\n    def test_connect_success(self):\n        \"\"\"Test successful connection.\"\"\"\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"test.sock\")\n\n            # Create a listening socket\n            server_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)\n            server_sock.bind(socket_path)\n            server_sock.listen(1)\n\n            try:\n                client = DirtyClient(socket_path)\n                client.connect()\n\n                assert client._sock is not None\n                client.close()\n            finally:\n                server_sock.close()\n\n    def test_close_idempotent(self):\n        \"\"\"Test that close can be called multiple times.\"\"\"\n        client = DirtyClient(\"/tmp/test.sock\")\n        client.close()\n        client.close()  # Should not raise\n\n\nclass TestDirtyClientAsync:\n    \"\"\"Tests for async API.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_connect_async_nonexistent_socket(self):\n        \"\"\"Test async connecting to non-existent socket.\"\"\"\n        client = DirtyClient(\"/nonexistent/socket.sock\", timeout=1.0)\n\n        with pytest.raises(DirtyConnectionError):\n            await client.connect_async()\n\n    @pytest.mark.asyncio\n    async def test_close_async_idempotent(self):\n        \"\"\"Test that close_async can be called multiple times.\"\"\"\n        client = DirtyClient(\"/tmp/test.sock\")\n        await client.close_async()\n        await client.close_async()  # Should not raise\n\n\nclass TestDirtyClientContextManagers:\n    \"\"\"Tests for context manager functionality.\"\"\"\n\n    def test_sync_context_manager_connection_error(self):\n        \"\"\"Test sync context manager with connection error.\"\"\"\n        client = DirtyClient(\"/nonexistent/socket.sock\")\n\n        with pytest.raises(DirtyConnectionError):\n            with client:\n                pass\n\n    @pytest.mark.asyncio\n    async def test_async_context_manager_connection_error(self):\n        \"\"\"Test async context manager with connection error.\"\"\"\n        client = DirtyClient(\"/nonexistent/socket.sock\", timeout=1.0)\n\n        with pytest.raises(DirtyConnectionError):\n            async with client:\n                pass\n\n\nclass TestDirtyClientHelpers:\n    \"\"\"Tests for helper functions.\"\"\"\n\n    def test_set_get_socket_path(self):\n        \"\"\"Test setting and getting socket path.\"\"\"\n        original = os.environ.get('GUNICORN_DIRTY_SOCKET')\n\n        try:\n            set_dirty_socket_path(\"/tmp/dirty.sock\")\n            assert get_dirty_socket_path() == \"/tmp/dirty.sock\"\n        finally:\n            set_dirty_socket_path(None)\n            if original:\n                os.environ['GUNICORN_DIRTY_SOCKET'] = original\n\n    def test_get_socket_path_from_env(self):\n        \"\"\"Test getting socket path from environment.\"\"\"\n        original = os.environ.get('GUNICORN_DIRTY_SOCKET')\n\n        try:\n            set_dirty_socket_path(None)\n            os.environ['GUNICORN_DIRTY_SOCKET'] = \"/env/dirty.sock\"\n            assert get_dirty_socket_path() == \"/env/dirty.sock\"\n        finally:\n            set_dirty_socket_path(None)\n            if original:\n                os.environ['GUNICORN_DIRTY_SOCKET'] = original\n            else:\n                os.environ.pop('GUNICORN_DIRTY_SOCKET', None)\n\n    def test_get_socket_path_not_configured(self):\n        \"\"\"Test error when socket path not configured.\"\"\"\n        original = os.environ.get('GUNICORN_DIRTY_SOCKET')\n\n        try:\n            set_dirty_socket_path(None)\n            os.environ.pop('GUNICORN_DIRTY_SOCKET', None)\n\n            with pytest.raises(DirtyError) as exc_info:\n                get_dirty_socket_path()\n            assert \"not configured\" in str(exc_info.value)\n        finally:\n            if original:\n                os.environ['GUNICORN_DIRTY_SOCKET'] = original\n\n    def test_get_dirty_client_thread_local(self):\n        \"\"\"Test that get_dirty_client returns thread-local client.\"\"\"\n        original = os.environ.get('GUNICORN_DIRTY_SOCKET')\n\n        try:\n            set_dirty_socket_path(\"/tmp/test.sock\")\n\n            # Clean up any existing client\n            close_dirty_client()\n\n            client1 = get_dirty_client()\n            client2 = get_dirty_client()\n\n            # Should return same instance in same thread\n            assert client1 is client2\n\n            close_dirty_client()\n        finally:\n            set_dirty_socket_path(None)\n            if original:\n                os.environ['GUNICORN_DIRTY_SOCKET'] = original\n\n    def test_get_dirty_client_different_threads(self):\n        \"\"\"Test that different threads get different clients.\"\"\"\n        original = os.environ.get('GUNICORN_DIRTY_SOCKET')\n        clients = []\n\n        try:\n            set_dirty_socket_path(\"/tmp/test.sock\")\n\n            def get_client():\n                clients.append(get_dirty_client())\n                close_dirty_client()\n\n            # Clean up main thread client\n            close_dirty_client()\n\n            t1 = threading.Thread(target=get_client)\n            t2 = threading.Thread(target=get_client)\n\n            t1.start()\n            t2.start()\n            t1.join()\n            t2.join()\n\n            # Different threads should get different clients\n            assert len(clients) == 2\n            assert clients[0] is not clients[1]\n        finally:\n            set_dirty_socket_path(None)\n            if original:\n                os.environ['GUNICORN_DIRTY_SOCKET'] = original\n\n    def test_close_dirty_client(self):\n        \"\"\"Test closing thread-local client.\"\"\"\n        original = os.environ.get('GUNICORN_DIRTY_SOCKET')\n\n        try:\n            set_dirty_socket_path(\"/tmp/test.sock\")\n\n            client = get_dirty_client()\n            close_dirty_client()\n\n            # Should be able to get a new client\n            client2 = get_dirty_client()\n            assert client2 is not client\n\n            close_dirty_client()\n        finally:\n            set_dirty_socket_path(None)\n            if original:\n                os.environ['GUNICORN_DIRTY_SOCKET'] = original\n\n\nclass TestDirtyClientResponseHandling:\n    \"\"\"Tests for response handling.\"\"\"\n\n    def test_handle_response_success(self):\n        \"\"\"Test handling successful response.\"\"\"\n        client = DirtyClient(\"/tmp/test.sock\")\n        response = make_response(\"test-id\", {\"data\": \"value\"})\n\n        result = client._handle_response(response)\n        assert result == {\"data\": \"value\"}\n\n    def test_handle_response_error(self):\n        \"\"\"Test handling error response.\"\"\"\n        client = DirtyClient(\"/tmp/test.sock\")\n        response = {\n            \"type\": DirtyProtocol.MSG_TYPE_ERROR,\n            \"id\": \"test-id\",\n            \"error\": {\n                \"error_type\": \"DirtyError\",\n                \"message\": \"Test error\",\n                \"details\": {},\n            },\n        }\n\n        with pytest.raises(DirtyError) as exc_info:\n            client._handle_response(response)\n        assert \"Test error\" in str(exc_info.value)\n\n    def test_handle_response_unknown_type(self):\n        \"\"\"Test handling unknown response type.\"\"\"\n        client = DirtyClient(\"/tmp/test.sock\")\n        response = {\n            \"type\": \"unknown\",\n            \"id\": \"test-id\",\n        }\n\n        with pytest.raises(DirtyError) as exc_info:\n            client._handle_response(response)\n        assert \"Unknown response type\" in str(exc_info.value)\n\n\nclass TestDirtyClientExecute:\n    \"\"\"Tests for execute functionality with mock sockets.\"\"\"\n\n    def test_execute_with_socket_pair(self):\n        \"\"\"Test execute using a socket pair to simulate server.\"\"\"\n        import threading\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"test.sock\")\n\n            # Create server socket\n            server_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)\n            server_sock.bind(socket_path)\n            server_sock.listen(1)\n\n            response_sent = threading.Event()\n\n            def server_handler():\n                conn, _ = server_sock.accept()\n                try:\n                    # Read request\n                    msg = DirtyProtocol.read_message(conn)\n                    # Send response\n                    resp = make_response(msg[\"id\"], {\"result\": \"success\"})\n                    DirtyProtocol.write_message(conn, resp)\n                    response_sent.set()\n                finally:\n                    conn.close()\n\n            server_thread = threading.Thread(target=server_handler)\n            server_thread.start()\n\n            try:\n                client = DirtyClient(socket_path, timeout=5.0)\n                result = client.execute(\"test:App\", \"action\", \"arg1\", key=\"value\")\n                assert result == {\"result\": \"success\"}\n                client.close()\n            finally:\n                response_sent.wait(timeout=2.0)\n                server_thread.join(timeout=2.0)\n                server_sock.close()\n\n    def test_close_socket_clears_sock(self):\n        \"\"\"Test that _close_socket clears the socket.\"\"\"\n        client = DirtyClient(\"/tmp/test.sock\")\n        # Simulate having a socket\n        client._sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)\n        client._close_socket()\n        assert client._sock is None\n"
  },
  {
    "path": "tests/test_dirty_config.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Tests for dirty arbiter configuration settings.\"\"\"\n\nimport pytest\n\nfrom gunicorn.config import Config\n\n\nclass TestDirtyConfig:\n    \"\"\"Tests for dirty arbiter configuration settings.\"\"\"\n\n    def test_dirty_apps_default(self):\n        \"\"\"Test dirty_apps default is empty list.\"\"\"\n        cfg = Config()\n        assert cfg.dirty_apps == []\n\n    def test_dirty_apps_single(self):\n        \"\"\"Test dirty_apps with single app.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_apps\", [\"myapp.ml:MLApp\"])\n        assert cfg.dirty_apps == [\"myapp.ml:MLApp\"]\n\n    def test_dirty_apps_multiple(self):\n        \"\"\"Test dirty_apps with multiple apps.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_apps\", [\n            \"myapp.ml:MLApp\",\n            \"myapp.images:ImageApp\",\n        ])\n        assert len(cfg.dirty_apps) == 2\n        assert \"myapp.ml:MLApp\" in cfg.dirty_apps\n        assert \"myapp.images:ImageApp\" in cfg.dirty_apps\n\n    def test_dirty_workers_default(self):\n        \"\"\"Test dirty_workers default is 0 (disabled).\"\"\"\n        cfg = Config()\n        assert cfg.dirty_workers == 0\n\n    def test_dirty_workers_set(self):\n        \"\"\"Test setting dirty_workers.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_workers\", 2)\n        assert cfg.dirty_workers == 2\n\n    def test_dirty_workers_invalid_negative(self):\n        \"\"\"Test dirty_workers rejects negative values.\"\"\"\n        cfg = Config()\n        with pytest.raises(ValueError):\n            cfg.set(\"dirty_workers\", -1)\n\n    def test_dirty_timeout_default(self):\n        \"\"\"Test dirty_timeout default is 300 seconds.\"\"\"\n        cfg = Config()\n        assert cfg.dirty_timeout == 300\n\n    def test_dirty_timeout_set(self):\n        \"\"\"Test setting dirty_timeout.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_timeout\", 600)\n        assert cfg.dirty_timeout == 600\n\n    def test_dirty_timeout_zero_disables(self):\n        \"\"\"Test dirty_timeout can be set to 0 to disable.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_timeout\", 0)\n        assert cfg.dirty_timeout == 0\n\n    def test_dirty_threads_default(self):\n        \"\"\"Test dirty_threads default is 1.\"\"\"\n        cfg = Config()\n        assert cfg.dirty_threads == 1\n\n    def test_dirty_threads_set(self):\n        \"\"\"Test setting dirty_threads.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_threads\", 4)\n        assert cfg.dirty_threads == 4\n\n    def test_dirty_graceful_timeout_default(self):\n        \"\"\"Test dirty_graceful_timeout default is 30 seconds.\"\"\"\n        cfg = Config()\n        assert cfg.dirty_graceful_timeout == 30\n\n    def test_dirty_graceful_timeout_set(self):\n        \"\"\"Test setting dirty_graceful_timeout.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_graceful_timeout\", 60)\n        assert cfg.dirty_graceful_timeout == 60\n\n    def test_all_dirty_settings_accessible(self):\n        \"\"\"Test all dirty settings are accessible.\"\"\"\n        cfg = Config()\n        # These should not raise AttributeError\n        _ = cfg.dirty_apps\n        _ = cfg.dirty_workers\n        _ = cfg.dirty_timeout\n        _ = cfg.dirty_threads\n        _ = cfg.dirty_graceful_timeout\n\n\nclass TestDirtyConfigCLI:\n    \"\"\"Tests for dirty arbiter CLI argument parsing.\"\"\"\n\n    def test_dirty_workers_cli(self):\n        \"\"\"Test --dirty-workers CLI argument.\"\"\"\n        cfg = Config()\n        parser = cfg.parser()\n        args = parser.parse_args([\"--dirty-workers\", \"3\"])\n        assert args.dirty_workers == 3\n\n    def test_dirty_timeout_cli(self):\n        \"\"\"Test --dirty-timeout CLI argument.\"\"\"\n        cfg = Config()\n        parser = cfg.parser()\n        args = parser.parse_args([\"--dirty-timeout\", \"600\"])\n        assert args.dirty_timeout == 600\n\n    def test_dirty_threads_cli(self):\n        \"\"\"Test --dirty-threads CLI argument.\"\"\"\n        cfg = Config()\n        parser = cfg.parser()\n        args = parser.parse_args([\"--dirty-threads\", \"8\"])\n        assert args.dirty_threads == 8\n\n    def test_dirty_graceful_timeout_cli(self):\n        \"\"\"Test --dirty-graceful-timeout CLI argument.\"\"\"\n        cfg = Config()\n        parser = cfg.parser()\n        args = parser.parse_args([\"--dirty-graceful-timeout\", \"45\"])\n        assert args.dirty_graceful_timeout == 45\n\n    def test_dirty_app_cli(self):\n        \"\"\"Test --dirty-app CLI argument (can be repeated).\"\"\"\n        cfg = Config()\n        parser = cfg.parser()\n        args = parser.parse_args([\n            \"--dirty-app\", \"myapp.ml:MLApp\",\n            \"--dirty-app\", \"myapp.images:ImageApp\",\n        ])\n        assert args.dirty_apps == [\"myapp.ml:MLApp\", \"myapp.images:ImageApp\"]\n"
  },
  {
    "path": "tests/test_dirty_errors.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Tests for dirty errors module.\"\"\"\n\nimport pytest\n\nfrom gunicorn.dirty.errors import (\n    DirtyError,\n    DirtyNoWorkersAvailableError,\n)\n\n\nclass TestDirtyNoWorkersAvailableError:\n    \"\"\"Tests for DirtyNoWorkersAvailableError exception.\"\"\"\n\n    def test_error_contains_app_path(self):\n        \"\"\"Error includes the app_path.\"\"\"\n        error = DirtyNoWorkersAvailableError(\"myapp:Model\")\n        assert error.app_path == \"myapp:Model\"\n        assert \"myapp:Model\" in str(error)\n        assert \"No workers available\" in str(error)\n\n    def test_error_with_custom_message(self):\n        \"\"\"Error can have a custom message.\"\"\"\n        error = DirtyNoWorkersAvailableError(\n            \"myapp:Model\",\n            message=\"Custom: no workers for heavy model\"\n        )\n        assert error.app_path == \"myapp:Model\"\n        assert \"Custom: no workers\" in str(error)\n\n    def test_error_serialization_roundtrip(self):\n        \"\"\"Error survives to_dict/from_dict cycle.\"\"\"\n        original = DirtyNoWorkersAvailableError(\"myapp.ml:HugeModel\")\n\n        # Serialize\n        data = original.to_dict()\n        assert data[\"error_type\"] == \"DirtyNoWorkersAvailableError\"\n        assert data[\"details\"][\"app_path\"] == \"myapp.ml:HugeModel\"\n\n        # Deserialize\n        restored = DirtyError.from_dict(data)\n        assert isinstance(restored, DirtyNoWorkersAvailableError)\n        assert restored.app_path == \"myapp.ml:HugeModel\"\n        assert \"No workers available\" in str(restored)\n\n    def test_error_is_dirty_error_subclass(self):\n        \"\"\"DirtyNoWorkersAvailableError is a DirtyError subclass.\"\"\"\n        error = DirtyNoWorkersAvailableError(\"app:Class\")\n        assert isinstance(error, DirtyError)\n\n    def test_web_app_can_catch_specific_error(self):\n        \"\"\"Web app can catch DirtyNoWorkersAvailableError specifically.\"\"\"\n        def simulate_execute():\n            raise DirtyNoWorkersAvailableError(\"myapp:HeavyModel\")\n\n        # Catch specific error\n        try:\n            simulate_execute()\n            assert False, \"Should have raised\"\n        except DirtyNoWorkersAvailableError as e:\n            assert e.app_path == \"myapp:HeavyModel\"\n\n    def test_can_catch_as_base_error(self):\n        \"\"\"Can catch DirtyNoWorkersAvailableError as DirtyError.\"\"\"\n        def simulate_execute():\n            raise DirtyNoWorkersAvailableError(\"myapp:Model\")\n\n        try:\n            simulate_execute()\n            assert False, \"Should have raised\"\n        except DirtyError as e:\n            # Should catch it as the base class\n            assert hasattr(e, \"app_path\")\n"
  },
  {
    "path": "tests/test_dirty_hooks.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Tests for dirty arbiter hooks.\"\"\"\n\nimport pytest\n\nfrom gunicorn.config import Config\n\n\nclass TestDirtyHooksConfig:\n    \"\"\"Tests for dirty hook configuration settings.\"\"\"\n\n    def test_on_dirty_starting_default(self):\n        \"\"\"Test on_dirty_starting default is a callable.\"\"\"\n        cfg = Config()\n        assert callable(cfg.on_dirty_starting)\n\n    def test_on_dirty_starting_custom(self):\n        \"\"\"Test setting custom on_dirty_starting hook.\"\"\"\n        hook_calls = []\n\n        def my_hook(arbiter):\n            hook_calls.append(arbiter)\n\n        cfg = Config()\n        cfg.set(\"on_dirty_starting\", my_hook)\n\n        # Call the hook\n        cfg.on_dirty_starting(\"test_arbiter\")\n\n        assert hook_calls == [\"test_arbiter\"]\n\n    def test_dirty_post_fork_default(self):\n        \"\"\"Test dirty_post_fork default is a callable.\"\"\"\n        cfg = Config()\n        assert callable(cfg.dirty_post_fork)\n\n    def test_dirty_post_fork_custom(self):\n        \"\"\"Test setting custom dirty_post_fork hook.\"\"\"\n        hook_calls = []\n\n        def my_hook(arbiter, worker):\n            hook_calls.append((arbiter, worker))\n\n        cfg = Config()\n        cfg.set(\"dirty_post_fork\", my_hook)\n\n        # Call the hook\n        cfg.dirty_post_fork(\"test_arbiter\", \"test_worker\")\n\n        assert hook_calls == [(\"test_arbiter\", \"test_worker\")]\n\n    def test_dirty_worker_init_default(self):\n        \"\"\"Test dirty_worker_init default is a callable.\"\"\"\n        cfg = Config()\n        assert callable(cfg.dirty_worker_init)\n\n    def test_dirty_worker_init_custom(self):\n        \"\"\"Test setting custom dirty_worker_init hook.\"\"\"\n        hook_calls = []\n\n        def my_hook(worker):\n            hook_calls.append(worker)\n\n        cfg = Config()\n        cfg.set(\"dirty_worker_init\", my_hook)\n\n        # Call the hook\n        cfg.dirty_worker_init(\"test_worker\")\n\n        assert hook_calls == [\"test_worker\"]\n\n    def test_dirty_worker_exit_default(self):\n        \"\"\"Test dirty_worker_exit default is a callable.\"\"\"\n        cfg = Config()\n        assert callable(cfg.dirty_worker_exit)\n\n    def test_dirty_worker_exit_custom(self):\n        \"\"\"Test setting custom dirty_worker_exit hook.\"\"\"\n        hook_calls = []\n\n        def my_hook(arbiter, worker):\n            hook_calls.append((arbiter, worker))\n\n        cfg = Config()\n        cfg.set(\"dirty_worker_exit\", my_hook)\n\n        # Call the hook\n        cfg.dirty_worker_exit(\"test_arbiter\", \"test_worker\")\n\n        assert hook_calls == [(\"test_arbiter\", \"test_worker\")]\n\n\nclass TestDirtyHooksValidation:\n    \"\"\"Tests for hook validation.\"\"\"\n\n    def test_on_dirty_starting_requires_callable(self):\n        \"\"\"Test that on_dirty_starting requires a callable.\"\"\"\n        cfg = Config()\n        with pytest.raises(TypeError):\n            cfg.set(\"on_dirty_starting\", \"not_a_callable\")\n\n    def test_dirty_post_fork_requires_callable(self):\n        \"\"\"Test that dirty_post_fork requires a callable.\"\"\"\n        cfg = Config()\n        with pytest.raises(TypeError):\n            cfg.set(\"dirty_post_fork\", 123)\n"
  },
  {
    "path": "tests/test_dirty_integration.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Integration tests for dirty arbiter with main arbiter.\"\"\"\n\nimport os\nimport struct\nimport pytest\n\nfrom gunicorn.arbiter import Arbiter\nfrom gunicorn.config import Config\nfrom gunicorn.app.base import BaseApplication\nfrom gunicorn.dirty.protocol import DirtyProtocol, BinaryProtocol, HEADER_SIZE\n\n\nclass MockStreamWriter:\n    \"\"\"Mock StreamWriter that captures written messages.\"\"\"\n\n    def __init__(self):\n        self.messages = []\n        self._buffer = b\"\"\n        self.closed = False\n\n    def write(self, data):\n        self._buffer += data\n\n    async def drain(self):\n        # Decode the buffer to extract messages using binary protocol\n        while len(self._buffer) >= HEADER_SIZE:\n            # Decode header to get payload length\n            _, _, length = BinaryProtocol.decode_header(\n                self._buffer[:HEADER_SIZE]\n            )\n            total_size = HEADER_SIZE + length\n            if len(self._buffer) >= total_size:\n                msg_data = self._buffer[:total_size]\n                self._buffer = self._buffer[total_size:]\n                # decode_message returns (msg_type_str, request_id, payload_dict)\n                msg_type_str, request_id, payload_dict = BinaryProtocol.decode_message(msg_data)\n                # Reconstruct the dict format for backwards compatibility\n                result = {\"type\": msg_type_str, \"id\": request_id}\n                result.update(payload_dict)\n                self.messages.append(result)\n            else:\n                break\n\n    def close(self):\n        self.closed = True\n\n    async def wait_closed(self):\n        pass\n\n    def get_extra_info(self, name):\n        return None\n\n\nclass SimpleDirtyTestApp(BaseApplication):\n    \"\"\"Simple test application for integration tests.\"\"\"\n\n    def __init__(self, options=None):\n        self.options = options or {}\n        self.cfg = None\n        super().__init__()\n\n    def load_config(self):\n        for key, value in self.options.items():\n            if key in self.cfg.settings:\n                self.cfg.set(key.lower(), value)\n\n    def load(self):\n        def app(environ, start_response):\n            status = '200 OK'\n            output = b'Hello World!'\n            response_headers = [('Content-type', 'text/plain'),\n                                ('Content-Length', str(len(output)))]\n            start_response(status, response_headers)\n            return [output]\n        return app\n\n\nclass TestArbiterDirtyIntegration:\n    \"\"\"Tests for arbiter integration with dirty arbiter.\"\"\"\n\n    def test_arbiter_init_with_dirty_config(self):\n        \"\"\"Test arbiter initializes with dirty configuration.\"\"\"\n        app = SimpleDirtyTestApp(options={\n            'dirty_workers': 2,\n            'dirty_apps': ['tests.support_dirty_app:TestDirtyApp'],\n            'bind': '127.0.0.1:0',\n        })\n\n        arbiter = Arbiter(app)\n\n        assert arbiter.dirty_arbiter_pid == 0\n        assert arbiter.dirty_arbiter is None\n        assert arbiter.cfg.dirty_workers == 2\n        assert arbiter.cfg.dirty_apps == ['tests.support_dirty_app:TestDirtyApp']\n\n    def test_arbiter_init_without_dirty_config(self):\n        \"\"\"Test arbiter initializes without dirty configuration.\"\"\"\n        app = SimpleDirtyTestApp(options={\n            'bind': '127.0.0.1:0',\n        })\n\n        arbiter = Arbiter(app)\n\n        assert arbiter.dirty_arbiter_pid == 0\n        assert arbiter.cfg.dirty_workers == 0\n        assert arbiter.cfg.dirty_apps == []\n\n\nclass TestDirtyIntegrationEnvironment:\n    \"\"\"Tests for environment setup.\"\"\"\n\n    def test_dirty_socket_env_var_set(self):\n        \"\"\"Test that GUNICORN_DIRTY_SOCKET env var is set when dirty arbiter spawns.\"\"\"\n        # This test would require actually spawning the dirty arbiter\n        # which involves forking. We'll skip this for unit tests.\n        pass\n\n\nclass TestDirtyExecutionTimeout:\n    \"\"\"Tests for execution timeout handling.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_worker_to_worker_communication(self):\n        \"\"\"Test protocol communication between worker and arbiter.\"\"\"\n        import asyncio\n        import tempfile\n        from gunicorn.dirty.worker import DirtyWorker\n        from gunicorn.dirty.protocol import DirtyProtocol, make_request\n\n        class MockLog:\n            def debug(self, *a, **kw): pass\n            def info(self, *a, **kw): pass\n            def warning(self, *a, **kw): pass\n            def error(self, *a, **kw): pass\n            def close_on_exec(self): pass\n            def reopen_files(self): pass\n\n        cfg = Config()\n        cfg.set(\"dirty_timeout\", 300)\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[\"tests.support_dirty_app:TestDirtyApp\"],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n            worker.pid = os.getpid()\n            worker.load_apps()\n\n            # Start worker server\n            server = await asyncio.start_unix_server(\n                worker.handle_connection,\n                path=socket_path\n            )\n\n            # Connect as client\n            reader, writer = await asyncio.open_unix_connection(socket_path)\n\n            # Send a request\n            request = make_request(\n                request_id=\"timeout-test-1\",\n                app_path=\"tests.support_dirty_app:TestDirtyApp\",\n                action=\"compute\",\n                args=(10, 5),\n                kwargs={\"operation\": \"add\"}\n            )\n\n            await DirtyProtocol.write_message_async(writer, request)\n\n            # Receive response\n            response = await DirtyProtocol.read_message_async(reader)\n\n            assert response[\"type\"] == DirtyProtocol.MSG_TYPE_RESPONSE\n            assert response[\"result\"] == 15\n\n            # Cleanup\n            writer.close()\n            await writer.wait_closed()\n            server.close()\n            await server.wait_closed()\n            worker._cleanup()\n\n    @pytest.mark.asyncio\n    async def test_arbiter_timeout_response(self):\n        \"\"\"Test that arbiter returns timeout error when worker doesn't respond.\"\"\"\n        import asyncio\n        import tempfile\n        from gunicorn.dirty.arbiter import DirtyArbiter\n        from gunicorn.dirty.protocol import DirtyProtocol, make_request\n\n        class MockLog:\n            def debug(self, *a, **kw): pass\n            def info(self, *a, **kw): pass\n            def warning(self, *a, **kw): pass\n            def error(self, *a, **kw): pass\n            def critical(self, *a, **kw): pass\n            def exception(self, *a, **kw): pass\n            def close_on_exec(self): pass\n            def reopen_files(self): pass\n\n        cfg = Config()\n        cfg.set(\"dirty_workers\", 0)\n        cfg.set(\"dirty_timeout\", 1)  # 1 second timeout\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"arbiter.sock\")\n            worker_socket_path = os.path.join(tmpdir, \"worker.sock\")\n\n            arbiter = DirtyArbiter(cfg=cfg, log=log, socket_path=socket_path)\n            arbiter.pid = os.getpid()\n            arbiter.alive = True\n            slow_server = None\n\n            try:\n                # Register a fake worker that will never respond\n                fake_pid = 99999\n                arbiter.workers[fake_pid] = \"fake_worker\"\n                arbiter.worker_sockets[fake_pid] = worker_socket_path\n\n                # Create a \"slow\" worker server that accepts but never responds\n                async def slow_client_handler(reader, writer):\n                    # Read the request but don't respond (simulating timeout)\n                    try:\n                        await asyncio.sleep(10)  # Longer than timeout\n                    except asyncio.CancelledError:\n                        pass\n                    finally:\n                        try:\n                            writer.close()\n                            await writer.wait_closed()\n                        except Exception:\n                            pass\n\n                slow_server = await asyncio.start_unix_server(\n                    slow_client_handler,\n                    path=worker_socket_path\n                )\n\n                request = make_request(\n                    request_id=\"timeout-test\",\n                    app_path=\"test:App\",\n                    action=\"slow_action\"\n                )\n\n                # Use MockStreamWriter to capture the response\n                mock_writer = MockStreamWriter()\n                await arbiter.route_request(request, mock_writer)\n\n                assert len(mock_writer.messages) == 1\n                response = mock_writer.messages[0]\n                assert response[\"type\"] == DirtyProtocol.MSG_TYPE_ERROR\n                assert \"timeout\" in response[\"error\"][\"error_type\"].lower()\n            finally:\n                # Cancel any pending consumer tasks\n                arbiter.alive = False\n                for task in arbiter.worker_consumers.values():\n                    task.cancel()\n                    try:\n                        await task\n                    except asyncio.CancelledError:\n                        pass\n\n                # Close worker connections\n                arbiter._close_worker_connection(fake_pid)\n\n                # Cleanup server\n                if slow_server:\n                    slow_server.close()\n                    await slow_server.wait_closed()\n\n                arbiter._cleanup_sync()\n\n    @pytest.mark.asyncio\n    async def test_full_request_response_flow(self):\n        \"\"\"Test full request-response flow between arbiter and worker.\"\"\"\n        import asyncio\n        import tempfile\n        from gunicorn.dirty.arbiter import DirtyArbiter\n        from gunicorn.dirty.worker import DirtyWorker\n        from gunicorn.dirty.protocol import DirtyProtocol, make_request\n\n        class MockLog:\n            def debug(self, *a, **kw): pass\n            def info(self, *a, **kw): pass\n            def warning(self, *a, **kw): pass\n            def error(self, *a, **kw): pass\n            def critical(self, *a, **kw): pass\n            def exception(self, *a, **kw): pass\n            def close_on_exec(self): pass\n            def reopen_files(self): pass\n\n        cfg = Config()\n        cfg.set(\"dirty_workers\", 0)\n        cfg.set(\"dirty_timeout\", 10)\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            arbiter_socket_path = os.path.join(tmpdir, \"arbiter.sock\")\n            worker_socket_path = os.path.join(tmpdir, \"worker.sock\")\n\n            worker = None\n            arbiter = None\n            worker_server = None\n            fake_pid = 12345\n\n            try:\n                # Create worker\n                worker = DirtyWorker(\n                    age=1,\n                    ppid=os.getpid(),\n                    app_paths=[\"tests.support_dirty_app:TestDirtyApp\"],\n                    cfg=cfg,\n                    log=log,\n                    socket_path=worker_socket_path\n                )\n                worker.pid = os.getpid()\n                worker.load_apps()\n\n                # Start worker server\n                worker_server = await asyncio.start_unix_server(\n                    worker.handle_connection,\n                    path=worker_socket_path\n                )\n\n                # Create arbiter\n                arbiter = DirtyArbiter(cfg=cfg, log=log, socket_path=arbiter_socket_path)\n                arbiter.pid = os.getpid()\n                arbiter.alive = True\n\n                # Register worker\n                arbiter.workers[fake_pid] = worker\n                arbiter.worker_sockets[fake_pid] = worker_socket_path\n\n                # Route a request using MockStreamWriter\n                request = make_request(\n                    request_id=\"full-flow-test\",\n                    app_path=\"tests.support_dirty_app:TestDirtyApp\",\n                    action=\"compute\",\n                    args=(7, 3),\n                    kwargs={\"operation\": \"multiply\"}\n                )\n\n                mock_writer = MockStreamWriter()\n                await arbiter.route_request(request, mock_writer)\n\n                assert len(mock_writer.messages) == 1\n                response = mock_writer.messages[0]\n                assert response[\"type\"] == DirtyProtocol.MSG_TYPE_RESPONSE\n                assert response[\"result\"] == 21\n            finally:\n                # Cancel any pending consumer tasks\n                if arbiter:\n                    arbiter.alive = False\n                    for task in arbiter.worker_consumers.values():\n                        task.cancel()\n                        try:\n                            await task\n                        except asyncio.CancelledError:\n                            pass\n\n                    # Close arbiter's connection first\n                    arbiter._close_worker_connection(fake_pid)\n                    arbiter._cleanup_sync()\n\n                # Close worker server\n                if worker_server:\n                    worker_server.close()\n                    await worker_server.wait_closed()\n\n                if worker:\n                    worker._cleanup()\n"
  },
  {
    "path": "tests/test_dirty_protocol.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Tests for dirty worker binary protocol module.\"\"\"\n\nimport asyncio\nimport os\nimport socket\nimport struct\nimport pytest\n\nfrom gunicorn.dirty.protocol import (\n    BinaryProtocol,\n    DirtyProtocol,\n    make_request,\n    make_response,\n    make_error_response,\n    make_chunk_message,\n    make_end_message,\n    MAGIC,\n    VERSION,\n    HEADER_SIZE,\n    HEADER_FORMAT,\n    MSG_TYPE_REQUEST,\n    MSG_TYPE_RESPONSE,\n    MSG_TYPE_ERROR,\n    MSG_TYPE_CHUNK,\n    MSG_TYPE_END,\n    MAX_MESSAGE_SIZE,\n)\nfrom gunicorn.dirty.errors import (\n    DirtyError,\n    DirtyProtocolError,\n    DirtyTimeoutError,\n    DirtyAppError,\n)\n\n\nclass TestBinaryProtocolHeader:\n    \"\"\"Tests for header encoding/decoding.\"\"\"\n\n    def test_header_size(self):\n        \"\"\"Test header size is 16 bytes.\"\"\"\n        assert HEADER_SIZE == 16\n\n    def test_encode_header(self):\n        \"\"\"Test header encoding.\"\"\"\n        header = BinaryProtocol.encode_header(MSG_TYPE_REQUEST, 12345, 100)\n        assert len(header) == HEADER_SIZE\n        assert header[:2] == MAGIC\n        assert header[2] == VERSION\n        assert header[3] == MSG_TYPE_REQUEST\n\n    def test_decode_header(self):\n        \"\"\"Test header decoding.\"\"\"\n        header = BinaryProtocol.encode_header(MSG_TYPE_RESPONSE, 67890, 200)\n        msg_type, request_id, length = BinaryProtocol.decode_header(header)\n        assert msg_type == MSG_TYPE_RESPONSE\n        assert request_id == 67890\n        assert length == 200\n\n    def test_decode_header_invalid_magic(self):\n        \"\"\"Test header decoding with invalid magic.\"\"\"\n        header = b\"XX\" + b\"\\x01\\x01\" + b\"\\x00\" * 12\n        with pytest.raises(DirtyProtocolError) as exc_info:\n            BinaryProtocol.decode_header(header)\n        assert \"magic\" in str(exc_info.value).lower()\n\n    def test_decode_header_invalid_version(self):\n        \"\"\"Test header decoding with invalid version.\"\"\"\n        header = MAGIC + b\"\\x99\\x01\" + b\"\\x00\" * 12\n        with pytest.raises(DirtyProtocolError) as exc_info:\n            BinaryProtocol.decode_header(header)\n        assert \"version\" in str(exc_info.value).lower()\n\n    def test_decode_header_invalid_type(self):\n        \"\"\"Test header decoding with invalid message type.\"\"\"\n        header = MAGIC + bytes([VERSION, 0xFF]) + b\"\\x00\" * 12\n        with pytest.raises(DirtyProtocolError) as exc_info:\n            BinaryProtocol.decode_header(header)\n        assert \"type\" in str(exc_info.value).lower()\n\n    def test_decode_header_too_large(self):\n        \"\"\"Test header decoding rejects too-large messages.\"\"\"\n        header = struct.pack(HEADER_FORMAT, MAGIC, VERSION, MSG_TYPE_REQUEST,\n                             MAX_MESSAGE_SIZE + 1, 0)\n        with pytest.raises(DirtyProtocolError) as exc_info:\n            BinaryProtocol.decode_header(header)\n        assert \"too large\" in str(exc_info.value).lower()\n\n    def test_decode_header_too_short(self):\n        \"\"\"Test header decoding with too-short data.\"\"\"\n        header = MAGIC + b\"\\x01\"\n        with pytest.raises(DirtyProtocolError) as exc_info:\n            BinaryProtocol.decode_header(header)\n        assert \"short\" in str(exc_info.value).lower()\n\n\nclass TestBinaryProtocolEncodeDecode:\n    \"\"\"Tests for message encoding/decoding.\"\"\"\n\n    def test_encode_decode_request(self):\n        \"\"\"Test request encoding/decoding roundtrip.\"\"\"\n        encoded = BinaryProtocol.encode_request(\n            request_id=12345,\n            app_path=\"myapp.ml:MLApp\",\n            action=\"predict\",\n            args=(\"data\",),\n            kwargs={\"temperature\": 0.7}\n        )\n        assert len(encoded) > HEADER_SIZE\n\n        msg_type_str, request_id, payload = BinaryProtocol.decode_message(encoded)\n        assert msg_type_str == \"request\"\n        assert request_id == 12345\n        assert payload[\"app_path\"] == \"myapp.ml:MLApp\"\n        assert payload[\"action\"] == \"predict\"\n        assert payload[\"args\"] == [\"data\"]\n        assert payload[\"kwargs\"] == {\"temperature\": 0.7}\n\n    def test_encode_decode_response(self):\n        \"\"\"Test response encoding/decoding roundtrip.\"\"\"\n        result = {\"predictions\": [0.1, 0.9], \"metadata\": {\"model\": \"v1\"}}\n        encoded = BinaryProtocol.encode_response(request_id=67890, result=result)\n\n        msg_type_str, request_id, payload = BinaryProtocol.decode_message(encoded)\n        assert msg_type_str == \"response\"\n        assert request_id == 67890\n        assert payload[\"result\"] == result\n\n    def test_encode_decode_error(self):\n        \"\"\"Test error encoding/decoding roundtrip.\"\"\"\n        error = DirtyTimeoutError(\"Timed out\", timeout=30)\n        encoded = BinaryProtocol.encode_error(request_id=11111, error=error)\n\n        msg_type_str, request_id, payload = BinaryProtocol.decode_message(encoded)\n        assert msg_type_str == \"error\"\n        assert request_id == 11111\n        assert payload[\"error\"][\"error_type\"] == \"DirtyTimeoutError\"\n        assert \"Timed out\" in payload[\"error\"][\"message\"]\n\n    def test_encode_decode_chunk(self):\n        \"\"\"Test chunk encoding/decoding roundtrip.\"\"\"\n        chunk_data = {\"token\": \"hello\", \"index\": 5}\n        encoded = BinaryProtocol.encode_chunk(request_id=22222, data=chunk_data)\n\n        msg_type_str, request_id, payload = BinaryProtocol.decode_message(encoded)\n        assert msg_type_str == \"chunk\"\n        assert request_id == 22222\n        assert payload[\"data\"] == chunk_data\n\n    def test_encode_decode_end(self):\n        \"\"\"Test end message encoding/decoding roundtrip.\"\"\"\n        encoded = BinaryProtocol.encode_end(request_id=33333)\n        assert len(encoded) == HEADER_SIZE  # End has no payload\n\n        msg_type_str, request_id, payload = BinaryProtocol.decode_message(encoded)\n        assert msg_type_str == \"end\"\n        assert request_id == 33333\n        assert payload == {}\n\n    def test_encode_decode_binary_data(self):\n        \"\"\"Test binary data passes through without base64 encoding.\"\"\"\n        binary_data = bytes(range(256))\n        encoded = BinaryProtocol.encode_response(\n            request_id=44444,\n            result={\"data\": binary_data}\n        )\n\n        msg_type_str, request_id, payload = BinaryProtocol.decode_message(encoded)\n        assert payload[\"result\"][\"data\"] == binary_data\n\n    def test_encode_decode_large_message(self):\n        \"\"\"Test encoding a large message.\"\"\"\n        large_data = b\"x\" * (1024 * 1024)  # 1 MB\n        encoded = BinaryProtocol.encode_response(\n            request_id=55555,\n            result={\"data\": large_data}\n        )\n\n        msg_type_str, request_id, payload = BinaryProtocol.decode_message(encoded)\n        assert payload[\"result\"][\"data\"] == large_data\n\n\nclass TestBinaryProtocolSync:\n    \"\"\"Tests for synchronous socket operations.\"\"\"\n\n    def test_read_write_message(self):\n        \"\"\"Test read/write through socket pair.\"\"\"\n        server_sock, client_sock = socket.socketpair()\n        try:\n            message = make_request(\n                request_id=12345,\n                app_path=\"test:App\",\n                action=\"run\"\n            )\n\n            BinaryProtocol.write_message(client_sock, message)\n            received = BinaryProtocol.read_message(server_sock)\n\n            assert received[\"type\"] == \"request\"\n            assert received[\"id\"] == hash(\"12345\") & 0xFFFFFFFFFFFFFFFF or \\\n                   received[\"id\"] == 12345\n            assert received[\"app_path\"] == \"test:App\"\n            assert received[\"action\"] == \"run\"\n        finally:\n            server_sock.close()\n            client_sock.close()\n\n    def test_read_write_with_int_id(self):\n        \"\"\"Test read/write with integer request ID.\"\"\"\n        server_sock, client_sock = socket.socketpair()\n        try:\n            message = {\n                \"type\": \"request\",\n                \"id\": 999888777,\n                \"app_path\": \"test:App\",\n                \"action\": \"run\",\n                \"args\": [],\n                \"kwargs\": {}\n            }\n\n            BinaryProtocol.write_message(client_sock, message)\n            received = BinaryProtocol.read_message(server_sock)\n\n            assert received[\"id\"] == 999888777\n        finally:\n            server_sock.close()\n            client_sock.close()\n\n    def test_multiple_messages(self):\n        \"\"\"Test sending multiple messages.\"\"\"\n        server_sock, client_sock = socket.socketpair()\n        try:\n            messages = [\n                make_request(i, f\"app{i}:App\", f\"action{i}\")\n                for i in range(1, 4)\n            ]\n\n            for msg in messages:\n                BinaryProtocol.write_message(client_sock, msg)\n\n            for i, _ in enumerate(messages, 1):\n                received = BinaryProtocol.read_message(server_sock)\n                assert received[\"app_path\"] == f\"app{i}:App\"\n                assert received[\"action\"] == f\"action{i}\"\n        finally:\n            server_sock.close()\n            client_sock.close()\n\n    def test_read_connection_closed(self):\n        \"\"\"Test reading from closed connection.\"\"\"\n        server_sock, client_sock = socket.socketpair()\n        client_sock.close()\n        with pytest.raises(DirtyProtocolError) as exc_info:\n            BinaryProtocol.read_message(server_sock)\n        assert \"closed\" in str(exc_info.value).lower()\n        server_sock.close()\n\n    def test_binary_data_roundtrip(self):\n        \"\"\"Test binary data roundtrip through socket.\"\"\"\n        server_sock, client_sock = socket.socketpair()\n        try:\n            binary_payload = b\"\\x00\\x01\\x02\\xff\\xfe\\xfd\"\n            message = make_response(12345, {\"binary\": binary_payload})\n\n            BinaryProtocol.write_message(client_sock, message)\n            received = BinaryProtocol.read_message(server_sock)\n\n            assert received[\"result\"][\"binary\"] == binary_payload\n        finally:\n            server_sock.close()\n            client_sock.close()\n\n\nclass TestBinaryProtocolAsync:\n    \"\"\"Tests for async stream operations.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_async_read_write(self):\n        \"\"\"Test async read/write with mock streams.\"\"\"\n        message = make_request(12345, \"test:App\", \"run\")\n\n        read_fd, write_fd = os.pipe()\n        try:\n            reader = asyncio.StreamReader()\n            _ = asyncio.StreamReaderProtocol(reader)\n\n            encoded = BinaryProtocol._encode_from_dict(message)\n            os.write(write_fd, encoded)\n            os.close(write_fd)\n            write_fd = None\n\n            data = os.read(read_fd, len(encoded))\n            reader.feed_data(data)\n            reader.feed_eof()\n\n            received = await BinaryProtocol.read_message_async(reader)\n            assert received[\"type\"] == \"request\"\n            assert received[\"app_path\"] == \"test:App\"\n        finally:\n            if write_fd is not None:\n                os.close(write_fd)\n            os.close(read_fd)\n\n    @pytest.mark.asyncio\n    async def test_async_read_incomplete_header(self):\n        \"\"\"Test async read with incomplete header.\"\"\"\n        reader = asyncio.StreamReader()\n        reader.feed_data(MAGIC + b\"\\x01\")  # Only 3 bytes\n        reader.feed_eof()\n\n        with pytest.raises((asyncio.IncompleteReadError, DirtyProtocolError)):\n            await BinaryProtocol.read_message_async(reader)\n\n    @pytest.mark.asyncio\n    async def test_async_read_empty_connection(self):\n        \"\"\"Test async read on empty connection.\"\"\"\n        reader = asyncio.StreamReader()\n        reader.feed_eof()\n\n        with pytest.raises(asyncio.IncompleteReadError):\n            await BinaryProtocol.read_message_async(reader)\n\n    @pytest.mark.asyncio\n    async def test_async_read_invalid_magic(self):\n        \"\"\"Test async read rejects invalid magic.\"\"\"\n        reader = asyncio.StreamReader()\n        header = b\"XX\" + bytes([VERSION, MSG_TYPE_REQUEST]) + b\"\\x00\" * 12\n        reader.feed_data(header)\n        reader.feed_eof()\n\n        with pytest.raises(DirtyProtocolError) as exc_info:\n            await BinaryProtocol.read_message_async(reader)\n        assert \"magic\" in str(exc_info.value).lower()\n\n    @pytest.mark.asyncio\n    async def test_async_read_message_too_large(self):\n        \"\"\"Test async read rejects too-large messages.\"\"\"\n        reader = asyncio.StreamReader()\n        header = struct.pack(HEADER_FORMAT, MAGIC, VERSION, MSG_TYPE_REQUEST,\n                             MAX_MESSAGE_SIZE + 1000, 0)\n        reader.feed_data(header)\n        reader.feed_eof()\n\n        with pytest.raises(DirtyProtocolError) as exc_info:\n            await BinaryProtocol.read_message_async(reader)\n        assert \"too large\" in str(exc_info.value)\n\n\nclass TestMessageBuilders:\n    \"\"\"Tests for message builder helper functions.\"\"\"\n\n    def test_make_request(self):\n        \"\"\"Test request message builder.\"\"\"\n        request = make_request(\n            request_id=\"abc123\",\n            app_path=\"myapp.ml:MLApp\",\n            action=\"inference\",\n            args=(\"model1\",),\n            kwargs={\"temperature\": 0.7}\n        )\n        assert request[\"type\"] == DirtyProtocol.MSG_TYPE_REQUEST\n        assert request[\"id\"] == \"abc123\"\n        assert request[\"app_path\"] == \"myapp.ml:MLApp\"\n        assert request[\"action\"] == \"inference\"\n        assert request[\"args\"] == [\"model1\"]\n        assert request[\"kwargs\"] == {\"temperature\": 0.7}\n\n    def test_make_request_minimal(self):\n        \"\"\"Test request with minimal arguments.\"\"\"\n        request = make_request(\n            request_id=\"abc\",\n            app_path=\"app:App\",\n            action=\"run\"\n        )\n        assert request[\"args\"] == []\n        assert request[\"kwargs\"] == {}\n\n    def test_make_response(self):\n        \"\"\"Test response message builder.\"\"\"\n        response = make_response(\n            request_id=\"abc123\",\n            result={\"status\": \"ok\", \"data\": [1, 2, 3]}\n        )\n        assert response[\"type\"] == DirtyProtocol.MSG_TYPE_RESPONSE\n        assert response[\"id\"] == \"abc123\"\n        assert response[\"result\"] == {\"status\": \"ok\", \"data\": [1, 2, 3]}\n\n    def test_make_error_response_with_exception(self):\n        \"\"\"Test error response with DirtyError.\"\"\"\n        error = DirtyTimeoutError(\"Operation timed out\", timeout=30)\n        response = make_error_response(\"abc123\", error)\n\n        assert response[\"type\"] == DirtyProtocol.MSG_TYPE_ERROR\n        assert response[\"id\"] == \"abc123\"\n        assert response[\"error\"][\"error_type\"] == \"DirtyTimeoutError\"\n        assert response[\"error\"][\"message\"] == \"Operation timed out\"\n        assert response[\"error\"][\"details\"][\"timeout\"] == 30\n\n    def test_make_error_response_with_dict(self):\n        \"\"\"Test error response with dict.\"\"\"\n        error_dict = {\n            \"error_type\": \"CustomError\",\n            \"message\": \"Something went wrong\",\n            \"details\": {\"code\": 500}\n        }\n        response = make_error_response(\"abc123\", error_dict)\n\n        assert response[\"error\"] == error_dict\n\n    def test_make_error_response_with_generic_exception(self):\n        \"\"\"Test error response with generic exception.\"\"\"\n        error = ValueError(\"Invalid value\")\n        response = make_error_response(\"abc123\", error)\n\n        assert response[\"error\"][\"error_type\"] == \"ValueError\"\n        assert response[\"error\"][\"message\"] == \"Invalid value\"\n\n    def test_make_chunk_message(self):\n        \"\"\"Test chunk message builder.\"\"\"\n        chunk = make_chunk_message(\"req-123\", \"Hello, \")\n        assert chunk[\"type\"] == DirtyProtocol.MSG_TYPE_CHUNK\n        assert chunk[\"id\"] == \"req-123\"\n        assert chunk[\"data\"] == \"Hello, \"\n\n    def test_make_chunk_message_with_complex_data(self):\n        \"\"\"Test chunk message with complex data.\"\"\"\n        data = {\"token\": \"world\", \"score\": 0.95, \"index\": 5}\n        chunk = make_chunk_message(\"req-456\", data)\n        assert chunk[\"type\"] == DirtyProtocol.MSG_TYPE_CHUNK\n        assert chunk[\"id\"] == \"req-456\"\n        assert chunk[\"data\"] == data\n\n    def test_make_chunk_message_with_binary_data(self):\n        \"\"\"Test chunk message with binary data.\"\"\"\n        data = b\"\\x00\\x01\\x02\\xff\"\n        chunk = make_chunk_message(\"req-789\", data)\n        assert chunk[\"data\"] == data\n\n    def test_make_end_message(self):\n        \"\"\"Test end message builder.\"\"\"\n        end = make_end_message(\"req-123\")\n        assert end[\"type\"] == DirtyProtocol.MSG_TYPE_END\n        assert end[\"id\"] == \"req-123\"\n        assert \"data\" not in end\n\n    def test_chunk_and_end_roundtrip(self):\n        \"\"\"Test that chunk and end messages can be encoded/decoded.\"\"\"\n        chunk = make_chunk_message(12345, {\"token\": \"hello\"})\n        end = make_end_message(12345)\n\n        # Test chunk roundtrip\n        encoded_chunk = BinaryProtocol._encode_from_dict(chunk)\n        msg_type, req_id, payload = BinaryProtocol.decode_message(encoded_chunk)\n        assert msg_type == \"chunk\"\n        assert payload[\"data\"] == {\"token\": \"hello\"}\n\n        # Test end roundtrip\n        encoded_end = BinaryProtocol._encode_from_dict(end)\n        msg_type, req_id, payload = BinaryProtocol.decode_message(encoded_end)\n        assert msg_type == \"end\"\n        assert payload == {}\n\n\nclass TestDirtyErrors:\n    \"\"\"Tests for error classes.\"\"\"\n\n    def test_dirty_error_to_dict(self):\n        \"\"\"Test serializing error to dict.\"\"\"\n        error = DirtyError(\"Test error\", {\"key\": \"value\"})\n        d = error.to_dict()\n        assert d[\"error_type\"] == \"DirtyError\"\n        assert d[\"message\"] == \"Test error\"\n        assert d[\"details\"] == {\"key\": \"value\"}\n\n    def test_dirty_error_from_dict(self):\n        \"\"\"Test deserializing error from dict.\"\"\"\n        d = {\n            \"error_type\": \"DirtyTimeoutError\",\n            \"message\": \"Timed out\",\n            \"details\": {\"timeout\": 30}\n        }\n        error = DirtyError.from_dict(d)\n        assert isinstance(error, DirtyTimeoutError)\n        assert error.message == \"Timed out\"\n        assert error.details[\"timeout\"] == 30\n\n    def test_dirty_error_from_dict_unknown_type(self):\n        \"\"\"Test deserializing unknown error type falls back to DirtyError.\"\"\"\n        d = {\n            \"error_type\": \"UnknownError\",\n            \"message\": \"Unknown\",\n            \"details\": {}\n        }\n        error = DirtyError.from_dict(d)\n        assert isinstance(error, DirtyError)\n        assert not isinstance(error, DirtyTimeoutError)\n\n    def test_dirty_app_error(self):\n        \"\"\"Test DirtyAppError fields.\"\"\"\n        error = DirtyAppError(\n            \"App failed\",\n            app_path=\"myapp:App\",\n            action=\"run\",\n            traceback=\"Traceback...\"\n        )\n        assert error.app_path == \"myapp:App\"\n        assert error.action == \"run\"\n        assert error.traceback == \"Traceback...\"\n        assert \"myapp:App\" in str(error)\n\n\nclass TestBackwardsCompatibility:\n    \"\"\"Tests for backwards compatibility with old JSON API.\"\"\"\n\n    def test_dirty_protocol_alias(self):\n        \"\"\"Test that DirtyProtocol is an alias for BinaryProtocol.\"\"\"\n        assert DirtyProtocol is BinaryProtocol\n\n    def test_header_size_attribute(self):\n        \"\"\"Test HEADER_SIZE is accessible on class.\"\"\"\n        assert DirtyProtocol.HEADER_SIZE == 16\n\n    def test_msg_type_constants(self):\n        \"\"\"Test message type constants are strings for compatibility.\"\"\"\n        assert DirtyProtocol.MSG_TYPE_REQUEST == \"request\"\n        assert DirtyProtocol.MSG_TYPE_RESPONSE == \"response\"\n        assert DirtyProtocol.MSG_TYPE_ERROR == \"error\"\n        assert DirtyProtocol.MSG_TYPE_CHUNK == \"chunk\"\n        assert DirtyProtocol.MSG_TYPE_END == \"end\"\n\n    def test_encode_decode_preserves_dict_format(self):\n        \"\"\"Test that read_message returns dict compatible with old API.\"\"\"\n        server_sock, client_sock = socket.socketpair()\n        try:\n            message = {\n                \"type\": \"response\",\n                \"id\": 12345,\n                \"result\": {\"status\": \"ok\"}\n            }\n\n            DirtyProtocol.write_message(client_sock, message)\n            received = DirtyProtocol.read_message(server_sock)\n\n            # Old API: access via dict keys\n            assert received[\"type\"] == \"response\"\n            assert received[\"result\"][\"status\"] == \"ok\"\n        finally:\n            server_sock.close()\n            client_sock.close()\n\n    def test_string_request_id_handled(self):\n        \"\"\"Test that string request IDs are handled (hashed to int).\"\"\"\n        server_sock, client_sock = socket.socketpair()\n        try:\n            message = make_request(\"uuid-string-id\", \"test:App\", \"run\")\n\n            DirtyProtocol.write_message(client_sock, message)\n            received = DirtyProtocol.read_message(server_sock)\n\n            # Request ID should be converted to int\n            assert isinstance(received[\"id\"], int)\n        finally:\n            server_sock.close()\n            client_sock.close()\n"
  },
  {
    "path": "tests/test_dirty_stash.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Tests for dirty stash (shared state) functionality.\"\"\"\n\nimport pytest\n\nfrom gunicorn.dirty.stash import (\n    StashClient,\n    StashTable,\n    StashError,\n    StashTableNotFoundError,\n    StashKeyNotFoundError,\n)\nfrom gunicorn.dirty.protocol import (\n    BinaryProtocol,\n    DirtyProtocol,\n    MSG_TYPE_STASH,\n    STASH_OP_PUT,\n    STASH_OP_GET,\n    STASH_OP_DELETE,\n    STASH_OP_KEYS,\n    STASH_OP_CLEAR,\n    STASH_OP_INFO,\n    STASH_OP_ENSURE,\n    STASH_OP_DELETE_TABLE,\n    STASH_OP_TABLES,\n    STASH_OP_EXISTS,\n    make_stash_message,\n)\n\n\nclass TestStashProtocol:\n    \"\"\"Test stash protocol encoding.\"\"\"\n\n    def test_make_stash_message_basic(self):\n        \"\"\"Test basic stash message creation.\"\"\"\n        msg = make_stash_message(123, STASH_OP_PUT, \"test_table\")\n        assert msg[\"type\"] == \"stash\"\n        assert msg[\"id\"] == 123\n        assert msg[\"op\"] == STASH_OP_PUT\n        assert msg[\"table\"] == \"test_table\"\n\n    def test_make_stash_message_with_key_value(self):\n        \"\"\"Test stash message with key and value.\"\"\"\n        msg = make_stash_message(\n            456, STASH_OP_PUT, \"sessions\",\n            key=\"user:1\", value={\"name\": \"Alice\"}\n        )\n        assert msg[\"key\"] == \"user:1\"\n        assert msg[\"value\"] == {\"name\": \"Alice\"}\n\n    def test_make_stash_message_with_pattern(self):\n        \"\"\"Test stash message with pattern.\"\"\"\n        msg = make_stash_message(\n            789, STASH_OP_KEYS, \"sessions\",\n            pattern=\"user:*\"\n        )\n        assert msg[\"pattern\"] == \"user:*\"\n\n    def test_encode_stash_message(self):\n        \"\"\"Test binary encoding of stash message.\"\"\"\n        msg = make_stash_message(\n            123, STASH_OP_PUT, \"test\",\n            key=\"k\", value=\"v\"\n        )\n        encoded = BinaryProtocol._encode_from_dict(msg)\n        assert isinstance(encoded, bytes)\n        assert len(encoded) > 16  # Header + payload\n\n    def test_stash_message_roundtrip(self):\n        \"\"\"Test encode/decode roundtrip for stash message.\"\"\"\n        original = make_stash_message(\n            12345, STASH_OP_GET, \"cache\",\n            key=\"my_key\"\n        )\n        encoded = BinaryProtocol._encode_from_dict(original)\n        msg_type, request_id, payload = BinaryProtocol.decode_message(encoded)\n\n        assert msg_type == \"stash\"\n        assert payload[\"op\"] == STASH_OP_GET\n        assert payload[\"table\"] == \"cache\"\n        assert payload[\"key\"] == \"my_key\"\n\n    def test_stash_operations_have_unique_codes(self):\n        \"\"\"Test that all stash operations have unique codes.\"\"\"\n        ops = [\n            STASH_OP_PUT,\n            STASH_OP_GET,\n            STASH_OP_DELETE,\n            STASH_OP_KEYS,\n            STASH_OP_CLEAR,\n            STASH_OP_INFO,\n            STASH_OP_ENSURE,\n            STASH_OP_DELETE_TABLE,\n            STASH_OP_TABLES,\n            STASH_OP_EXISTS,\n        ]\n        assert len(ops) == len(set(ops))\n\n\nclass TestStashTable:\n    \"\"\"Test StashTable dict-like interface.\"\"\"\n\n    def test_stash_table_name(self):\n        \"\"\"Test StashTable name property.\"\"\"\n        # Create a mock client\n        class MockClient:\n            pass\n\n        table = StashTable(MockClient(), \"test_table\")\n        assert table.name == \"test_table\"\n\n\nclass TestStashErrors:\n    \"\"\"Test stash error classes.\"\"\"\n\n    def test_stash_error_base(self):\n        \"\"\"Test base StashError.\"\"\"\n        error = StashError(\"test error\")\n        assert str(error) == \"test error\"\n        assert isinstance(error, Exception)\n\n    def test_stash_table_not_found_error(self):\n        \"\"\"Test StashTableNotFoundError.\"\"\"\n        error = StashTableNotFoundError(\"my_table\")\n        assert error.table_name == \"my_table\"\n        assert \"my_table\" in str(error)\n\n    def test_stash_key_not_found_error(self):\n        \"\"\"Test StashKeyNotFoundError.\"\"\"\n        error = StashKeyNotFoundError(\"my_table\", \"my_key\")\n        assert error.table_name == \"my_table\"\n        assert error.key == \"my_key\"\n        assert \"my_key\" in str(error)\n\n\nclass TestStashProtocolConstants:\n    \"\"\"Test protocol constants for stash.\"\"\"\n\n    def test_msg_type_stash_exists(self):\n        \"\"\"Test MSG_TYPE_STASH constant exists.\"\"\"\n        assert MSG_TYPE_STASH == 0x10\n\n    def test_dirty_protocol_exports_stash_type(self):\n        \"\"\"Test DirtyProtocol exports stash type.\"\"\"\n        assert DirtyProtocol.MSG_TYPE_STASH == \"stash\"\n\n    def test_stash_op_codes(self):\n        \"\"\"Test stash operation codes are integers.\"\"\"\n        assert isinstance(STASH_OP_PUT, int)\n        assert isinstance(STASH_OP_GET, int)\n        assert isinstance(STASH_OP_DELETE, int)\n        assert isinstance(STASH_OP_KEYS, int)\n        assert isinstance(STASH_OP_CLEAR, int)\n        assert isinstance(STASH_OP_INFO, int)\n        assert isinstance(STASH_OP_ENSURE, int)\n        assert isinstance(STASH_OP_DELETE_TABLE, int)\n        assert isinstance(STASH_OP_TABLES, int)\n        assert isinstance(STASH_OP_EXISTS, int)\n\n\nclass TestStashEncodingEdgeCases:\n    \"\"\"Test edge cases in stash encoding.\"\"\"\n\n    def test_encode_empty_table_name(self):\n        \"\"\"Test encoding with empty table name.\"\"\"\n        msg = make_stash_message(1, STASH_OP_TABLES, \"\")\n        encoded = BinaryProtocol._encode_from_dict(msg)\n        assert isinstance(encoded, bytes)\n\n    def test_encode_unicode_table_name(self):\n        \"\"\"Test encoding with unicode table name.\"\"\"\n        msg = make_stash_message(1, STASH_OP_PUT, \"テスト\", key=\"k\", value=\"v\")\n        encoded = BinaryProtocol._encode_from_dict(msg)\n        _, _, payload = BinaryProtocol.decode_message(encoded)\n        assert payload[\"table\"] == \"テスト\"\n\n    def test_encode_complex_value(self):\n        \"\"\"Test encoding with complex nested value.\"\"\"\n        value = {\n            \"name\": \"test\",\n            \"count\": 42,\n            \"nested\": {\"a\": [1, 2, 3]},\n            \"data\": b\"binary data\",\n        }\n        msg = make_stash_message(1, STASH_OP_PUT, \"test\", key=\"k\", value=value)\n        encoded = BinaryProtocol._encode_from_dict(msg)\n        _, _, payload = BinaryProtocol.decode_message(encoded)\n        assert payload[\"value\"] == value\n\n    def test_encode_none_key(self):\n        \"\"\"Test encoding with None key (for table-level ops).\"\"\"\n        msg = make_stash_message(1, STASH_OP_TABLES, \"\")\n        assert \"key\" not in msg\n\n    def test_encode_special_characters_in_pattern(self):\n        \"\"\"Test encoding with special characters in pattern.\"\"\"\n        msg = make_stash_message(\n            1, STASH_OP_KEYS, \"test\",\n            pattern=\"user:*:session:?\"\n        )\n        encoded = BinaryProtocol._encode_from_dict(msg)\n        _, _, payload = BinaryProtocol.decode_message(encoded)\n        assert payload[\"pattern\"] == \"user:*:session:?\"\n"
  },
  {
    "path": "tests/test_dirty_tlv.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Tests for dirty TLV binary encoder/decoder.\"\"\"\n\nimport math\nimport struct\nimport pytest\n\nfrom gunicorn.dirty.tlv import (\n    TLVEncoder,\n    TYPE_NONE,\n    TYPE_BOOL,\n    TYPE_INT64,\n    TYPE_FLOAT64,\n    TYPE_BYTES,\n    TYPE_STRING,\n    TYPE_LIST,\n    TYPE_DICT,\n    MAX_STRING_SIZE,\n    MAX_BYTES_SIZE,\n    MAX_LIST_SIZE,\n    MAX_DICT_SIZE,\n)\nfrom gunicorn.dirty.errors import DirtyProtocolError\n\n\nclass TestTLVEncoderBasicTypes:\n    \"\"\"Tests for basic type encoding/decoding.\"\"\"\n\n    def test_encode_decode_none(self):\n        \"\"\"Test None encoding/decoding.\"\"\"\n        encoded = TLVEncoder.encode(None)\n        assert encoded == bytes([TYPE_NONE])\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert value is None\n        assert offset == 1\n\n    def test_encode_decode_true(self):\n        \"\"\"Test True encoding/decoding.\"\"\"\n        encoded = TLVEncoder.encode(True)\n        assert encoded == bytes([TYPE_BOOL, 0x01])\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert value is True\n        assert offset == 2\n\n    def test_encode_decode_false(self):\n        \"\"\"Test False encoding/decoding.\"\"\"\n        encoded = TLVEncoder.encode(False)\n        assert encoded == bytes([TYPE_BOOL, 0x00])\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert value is False\n        assert offset == 2\n\n    def test_encode_decode_positive_int(self):\n        \"\"\"Test positive integer encoding/decoding.\"\"\"\n        encoded = TLVEncoder.encode(42)\n        assert encoded[0] == TYPE_INT64\n        assert len(encoded) == 9  # 1 type + 8 value\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert value == 42\n        assert offset == 9\n\n    def test_encode_decode_negative_int(self):\n        \"\"\"Test negative integer encoding/decoding.\"\"\"\n        encoded = TLVEncoder.encode(-12345)\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert value == -12345\n\n    def test_encode_decode_large_int(self):\n        \"\"\"Test large integer encoding/decoding.\"\"\"\n        large_val = 2**62\n        encoded = TLVEncoder.encode(large_val)\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert value == large_val\n\n    def test_encode_decode_zero(self):\n        \"\"\"Test zero encoding/decoding.\"\"\"\n        encoded = TLVEncoder.encode(0)\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert value == 0\n\n    def test_encode_decode_float(self):\n        \"\"\"Test float encoding/decoding.\"\"\"\n        encoded = TLVEncoder.encode(3.14159)\n        assert encoded[0] == TYPE_FLOAT64\n        assert len(encoded) == 9  # 1 type + 8 value\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert abs(value - 3.14159) < 1e-10\n\n    def test_encode_decode_negative_float(self):\n        \"\"\"Test negative float encoding/decoding.\"\"\"\n        encoded = TLVEncoder.encode(-273.15)\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert abs(value - (-273.15)) < 1e-10\n\n    def test_encode_decode_float_infinity(self):\n        \"\"\"Test infinity encoding/decoding.\"\"\"\n        encoded = TLVEncoder.encode(float('inf'))\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert value == float('inf')\n\n    def test_encode_decode_float_nan(self):\n        \"\"\"Test NaN encoding/decoding.\"\"\"\n        encoded = TLVEncoder.encode(float('nan'))\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert math.isnan(value)\n\n\nclass TestTLVEncoderBytes:\n    \"\"\"Tests for bytes encoding/decoding.\"\"\"\n\n    def test_encode_decode_empty_bytes(self):\n        \"\"\"Test empty bytes encoding/decoding.\"\"\"\n        encoded = TLVEncoder.encode(b\"\")\n        assert encoded[0] == TYPE_BYTES\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert value == b\"\"\n\n    def test_encode_decode_bytes(self):\n        \"\"\"Test bytes encoding/decoding.\"\"\"\n        data = b\"\\x00\\x01\\x02\\xff\\xfe\\xfd\"\n        encoded = TLVEncoder.encode(data)\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert value == data\n\n    def test_encode_decode_large_bytes(self):\n        \"\"\"Test large bytes encoding/decoding.\"\"\"\n        data = b\"x\" * 10000\n        encoded = TLVEncoder.encode(data)\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert value == data\n\n    def test_bytes_too_large(self):\n        \"\"\"Test that bytes exceeding max size raises error.\"\"\"\n        # We won't actually allocate MAX_BYTES_SIZE, just check the encoding\n        with pytest.raises(DirtyProtocolError) as exc_info:\n            TLVEncoder.encode(b\"x\" * (MAX_BYTES_SIZE + 1))\n        assert \"too large\" in str(exc_info.value).lower()\n\n\nclass TestTLVEncoderString:\n    \"\"\"Tests for string encoding/decoding.\"\"\"\n\n    def test_encode_decode_empty_string(self):\n        \"\"\"Test empty string encoding/decoding.\"\"\"\n        encoded = TLVEncoder.encode(\"\")\n        assert encoded[0] == TYPE_STRING\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert value == \"\"\n\n    def test_encode_decode_ascii_string(self):\n        \"\"\"Test ASCII string encoding/decoding.\"\"\"\n        encoded = TLVEncoder.encode(\"hello world\")\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert value == \"hello world\"\n\n    def test_encode_decode_unicode_string(self):\n        \"\"\"Test Unicode string encoding/decoding.\"\"\"\n        text = \"Hello, world! \\u00a9 \\u2603 \\U0001F600\"\n        encoded = TLVEncoder.encode(text)\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert value == text\n\n    def test_encode_decode_chinese(self):\n        \"\"\"Test Chinese characters encoding/decoding.\"\"\"\n        text = \"Hello, world!\"\n        encoded = TLVEncoder.encode(text)\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert value == text\n\n    def test_encode_decode_emoji(self):\n        \"\"\"Test emoji encoding/decoding.\"\"\"\n        text = \"Test emoji\"\n        encoded = TLVEncoder.encode(text)\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert value == text\n\n    def test_encode_decode_large_string(self):\n        \"\"\"Test large string encoding/decoding.\"\"\"\n        text = \"x\" * 10000\n        encoded = TLVEncoder.encode(text)\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert value == text\n\n\nclass TestTLVEncoderList:\n    \"\"\"Tests for list encoding/decoding.\"\"\"\n\n    def test_encode_decode_empty_list(self):\n        \"\"\"Test empty list encoding/decoding.\"\"\"\n        encoded = TLVEncoder.encode([])\n        assert encoded[0] == TYPE_LIST\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert value == []\n\n    def test_encode_decode_simple_list(self):\n        \"\"\"Test simple list encoding/decoding.\"\"\"\n        data = [1, 2, 3]\n        encoded = TLVEncoder.encode(data)\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert value == data\n\n    def test_encode_decode_mixed_list(self):\n        \"\"\"Test mixed type list encoding/decoding.\"\"\"\n        data = [1, \"hello\", 3.14, True, None, b\"bytes\"]\n        encoded = TLVEncoder.encode(data)\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert value == data\n\n    def test_encode_decode_nested_list(self):\n        \"\"\"Test nested list encoding/decoding.\"\"\"\n        data = [[1, 2], [3, [4, 5]], [\"a\", \"b\"]]\n        encoded = TLVEncoder.encode(data)\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert value == data\n\n    def test_encode_decode_tuple_as_list(self):\n        \"\"\"Test that tuples are encoded as lists.\"\"\"\n        data = (1, 2, 3)\n        encoded = TLVEncoder.encode(data)\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert value == [1, 2, 3]  # Decoded as list\n\n    def test_encode_decode_large_list(self):\n        \"\"\"Test large list encoding/decoding.\"\"\"\n        data = list(range(1000))\n        encoded = TLVEncoder.encode(data)\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert value == data\n\n\nclass TestTLVEncoderDict:\n    \"\"\"Tests for dict encoding/decoding.\"\"\"\n\n    def test_encode_decode_empty_dict(self):\n        \"\"\"Test empty dict encoding/decoding.\"\"\"\n        encoded = TLVEncoder.encode({})\n        assert encoded[0] == TYPE_DICT\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert value == {}\n\n    def test_encode_decode_simple_dict(self):\n        \"\"\"Test simple dict encoding/decoding.\"\"\"\n        data = {\"a\": 1, \"b\": 2, \"c\": 3}\n        encoded = TLVEncoder.encode(data)\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert value == data\n\n    def test_encode_decode_mixed_values_dict(self):\n        \"\"\"Test dict with mixed value types.\"\"\"\n        data = {\n            \"int\": 42,\n            \"float\": 3.14,\n            \"string\": \"hello\",\n            \"bool\": True,\n            \"none\": None,\n            \"bytes\": b\"data\",\n            \"list\": [1, 2, 3],\n        }\n        encoded = TLVEncoder.encode(data)\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert value == data\n\n    def test_encode_decode_nested_dict(self):\n        \"\"\"Test nested dict encoding/decoding.\"\"\"\n        data = {\n            \"outer\": {\n                \"inner\": {\n                    \"value\": 42\n                },\n                \"list\": [{\"a\": 1}, {\"b\": 2}]\n            }\n        }\n        encoded = TLVEncoder.encode(data)\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert value == data\n\n    def test_encode_dict_non_string_key_converted(self):\n        \"\"\"Test that non-string keys are converted to strings (like JSON).\"\"\"\n        data = {1: \"value\", 2: \"other\"}\n        encoded = TLVEncoder.encode(data)\n        decoded, _ = TLVEncoder.decode(encoded, 0)\n        # Keys should be converted to strings\n        assert decoded == {\"1\": \"value\", \"2\": \"other\"}\n\n\nclass TestTLVEncoderComplexStructures:\n    \"\"\"Tests for complex nested structures.\"\"\"\n\n    def test_encode_decode_request_like(self):\n        \"\"\"Test encoding/decoding a request-like structure.\"\"\"\n        data = {\n            \"id\": 12345,\n            \"app_path\": \"myapp.ml:MLApp\",\n            \"action\": \"predict\",\n            \"args\": [b\"input_data\", 0.7],\n            \"kwargs\": {\"temperature\": 0.7, \"max_tokens\": 1000},\n        }\n        encoded = TLVEncoder.encode(data)\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert value == data\n\n    def test_encode_decode_response_like(self):\n        \"\"\"Test encoding/decoding a response-like structure.\"\"\"\n        data = {\n            \"id\": 12345,\n            \"result\": {\n                \"predictions\": [0.1, 0.2, 0.7],\n                \"metadata\": {\"model\": \"v1.0\", \"latency_ms\": 42},\n            }\n        }\n        encoded = TLVEncoder.encode(data)\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert value == data\n\n    def test_encode_decode_deeply_nested(self):\n        \"\"\"Test deeply nested structures.\"\"\"\n        data = {\"a\": {\"b\": {\"c\": {\"d\": {\"e\": {\"f\": \"deep\"}}}}}}\n        encoded = TLVEncoder.encode(data)\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert value == data\n\n\nclass TestTLVEncoderRoundtrip:\n    \"\"\"Tests for complete roundtrip using decode_full.\"\"\"\n\n    def test_decode_full_simple(self):\n        \"\"\"Test decode_full with simple value.\"\"\"\n        data = {\"key\": \"value\"}\n        encoded = TLVEncoder.encode(data)\n\n        value = TLVEncoder.decode_full(encoded)\n        assert value == data\n\n    def test_decode_full_trailing_data(self):\n        \"\"\"Test decode_full raises on trailing data.\"\"\"\n        encoded = TLVEncoder.encode(42) + b\"extra\"\n\n        with pytest.raises(DirtyProtocolError) as exc_info:\n            TLVEncoder.decode_full(encoded)\n        assert \"trailing\" in str(exc_info.value).lower()\n\n\nclass TestTLVEncoderErrors:\n    \"\"\"Tests for error handling.\"\"\"\n\n    def test_decode_empty_data(self):\n        \"\"\"Test decoding empty data raises error.\"\"\"\n        with pytest.raises(DirtyProtocolError) as exc_info:\n            TLVEncoder.decode(b\"\", 0)\n        assert \"truncated\" in str(exc_info.value).lower()\n\n    def test_decode_truncated_int(self):\n        \"\"\"Test decoding truncated int raises error.\"\"\"\n        # TYPE_INT64 followed by only 4 bytes instead of 8\n        data = bytes([TYPE_INT64, 0, 0, 0, 0])\n        with pytest.raises(DirtyProtocolError) as exc_info:\n            TLVEncoder.decode(data, 0)\n        assert \"truncated\" in str(exc_info.value).lower()\n\n    def test_decode_truncated_float(self):\n        \"\"\"Test decoding truncated float raises error.\"\"\"\n        data = bytes([TYPE_FLOAT64, 0, 0, 0, 0])\n        with pytest.raises(DirtyProtocolError) as exc_info:\n            TLVEncoder.decode(data, 0)\n        assert \"truncated\" in str(exc_info.value).lower()\n\n    def test_decode_truncated_bytes_length(self):\n        \"\"\"Test decoding truncated bytes length raises error.\"\"\"\n        data = bytes([TYPE_BYTES, 0, 0])  # Only 2 bytes of length\n        with pytest.raises(DirtyProtocolError) as exc_info:\n            TLVEncoder.decode(data, 0)\n        assert \"truncated\" in str(exc_info.value).lower()\n\n    def test_decode_truncated_bytes_data(self):\n        \"\"\"Test decoding truncated bytes data raises error.\"\"\"\n        # Says 10 bytes but only provides 5\n        data = bytes([TYPE_BYTES]) + struct.pack(\">I\", 10) + b\"12345\"\n        with pytest.raises(DirtyProtocolError) as exc_info:\n            TLVEncoder.decode(data, 0)\n        assert \"truncated\" in str(exc_info.value).lower()\n\n    def test_decode_truncated_string_length(self):\n        \"\"\"Test decoding truncated string length raises error.\"\"\"\n        data = bytes([TYPE_STRING, 0])\n        with pytest.raises(DirtyProtocolError) as exc_info:\n            TLVEncoder.decode(data, 0)\n        assert \"truncated\" in str(exc_info.value).lower()\n\n    def test_decode_truncated_string_data(self):\n        \"\"\"Test decoding truncated string data raises error.\"\"\"\n        data = bytes([TYPE_STRING]) + struct.pack(\">I\", 10) + b\"hello\"\n        with pytest.raises(DirtyProtocolError) as exc_info:\n            TLVEncoder.decode(data, 0)\n        assert \"truncated\" in str(exc_info.value).lower()\n\n    def test_decode_invalid_utf8(self):\n        \"\"\"Test decoding invalid UTF-8 raises error.\"\"\"\n        # Valid length, but invalid UTF-8 bytes\n        data = bytes([TYPE_STRING]) + struct.pack(\">I\", 3) + b\"\\x80\\x81\\x82\"\n        with pytest.raises(DirtyProtocolError) as exc_info:\n            TLVEncoder.decode(data, 0)\n        assert \"utf-8\" in str(exc_info.value).lower()\n\n    def test_decode_truncated_list_count(self):\n        \"\"\"Test decoding truncated list count raises error.\"\"\"\n        data = bytes([TYPE_LIST, 0])\n        with pytest.raises(DirtyProtocolError) as exc_info:\n            TLVEncoder.decode(data, 0)\n        assert \"truncated\" in str(exc_info.value).lower()\n\n    def test_decode_truncated_dict_count(self):\n        \"\"\"Test decoding truncated dict count raises error.\"\"\"\n        data = bytes([TYPE_DICT, 0])\n        with pytest.raises(DirtyProtocolError) as exc_info:\n            TLVEncoder.decode(data, 0)\n        assert \"truncated\" in str(exc_info.value).lower()\n\n    def test_decode_unknown_type(self):\n        \"\"\"Test decoding unknown type raises error.\"\"\"\n        data = bytes([0xFF])  # Unknown type\n        with pytest.raises(DirtyProtocolError) as exc_info:\n            TLVEncoder.decode(data, 0)\n        assert \"unknown\" in str(exc_info.value).lower()\n\n    def test_encode_unsupported_type(self):\n        \"\"\"Test encoding unsupported type raises error.\"\"\"\n        with pytest.raises(DirtyProtocolError) as exc_info:\n            TLVEncoder.encode(object())\n        assert \"unsupported type\" in str(exc_info.value).lower()\n\n    def test_encode_function_raises_error(self):\n        \"\"\"Test encoding a function raises error.\"\"\"\n        with pytest.raises(DirtyProtocolError) as exc_info:\n            TLVEncoder.encode(lambda x: x)\n        assert \"unsupported type\" in str(exc_info.value).lower()\n\n    def test_decode_dict_non_string_key_in_data(self):\n        \"\"\"Test decoding dict with non-string key raises error.\"\"\"\n        # Manually construct a dict with int key\n        # TYPE_DICT, count=1, TYPE_INT64 key, TYPE_INT64 value\n        data = (\n            bytes([TYPE_DICT])\n            + struct.pack(\">I\", 1)\n            + bytes([TYPE_INT64])\n            + struct.pack(\">q\", 1)  # Key (int, not string)\n            + bytes([TYPE_INT64])\n            + struct.pack(\">q\", 2)  # Value\n        )\n        with pytest.raises(DirtyProtocolError) as exc_info:\n            TLVEncoder.decode(data, 0)\n        assert \"string\" in str(exc_info.value).lower()\n\n\nclass TestTLVEncoderOffset:\n    \"\"\"Tests for offset handling.\"\"\"\n\n    def test_decode_with_offset(self):\n        \"\"\"Test decoding from specific offset.\"\"\"\n        # Create data with prefix\n        prefix = b\"garbage\"\n        encoded = TLVEncoder.encode(42)\n        data = prefix + encoded\n\n        value, offset = TLVEncoder.decode(data, len(prefix))\n        assert value == 42\n        assert offset == len(prefix) + len(encoded)\n\n    def test_decode_multiple_values(self):\n        \"\"\"Test decoding multiple consecutive values.\"\"\"\n        v1 = TLVEncoder.encode(\"hello\")\n        v2 = TLVEncoder.encode(42)\n        v3 = TLVEncoder.encode([1, 2, 3])\n        data = v1 + v2 + v3\n\n        offset = 0\n        val1, offset = TLVEncoder.decode(data, offset)\n        assert val1 == \"hello\"\n\n        val2, offset = TLVEncoder.decode(data, offset)\n        assert val2 == 42\n\n        val3, offset = TLVEncoder.decode(data, offset)\n        assert val3 == [1, 2, 3]\n\n        assert offset == len(data)\n\n\nclass TestTLVEncoderBinaryData:\n    \"\"\"Tests for binary data handling (the main motivation for this protocol).\"\"\"\n\n    def test_binary_data_no_encoding(self):\n        \"\"\"Test that binary data is passed through without encoding.\"\"\"\n        # This is the key advantage over JSON - binary data doesn't need base64\n        binary_data = bytes(range(256))  # All byte values\n        encoded = TLVEncoder.encode(binary_data)\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert value == binary_data\n\n    def test_binary_with_null_bytes(self):\n        \"\"\"Test binary data with embedded null bytes.\"\"\"\n        binary_data = b\"\\x00\\x00\\xff\\x00\\x00\"\n        encoded = TLVEncoder.encode(binary_data)\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert value == binary_data\n\n    def test_binary_in_nested_structure(self):\n        \"\"\"Test binary data inside nested structures.\"\"\"\n        data = {\n            \"image\": b\"\\x89PNG\\r\\n\\x1a\\n\" + b\"\\x00\" * 100,\n            \"metadata\": {\"width\": 640, \"height\": 480},\n            \"chunks\": [b\"chunk1\", b\"chunk2\", b\"chunk3\"],\n        }\n        encoded = TLVEncoder.encode(data)\n\n        value, offset = TLVEncoder.decode(encoded, 0)\n        assert value == data\n"
  },
  {
    "path": "tests/test_dirty_worker.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Tests for dirty worker module.\"\"\"\n\nimport asyncio\nimport os\nimport signal\nimport tempfile\nimport pytest\n\nfrom gunicorn.config import Config\nfrom gunicorn.dirty.worker import DirtyWorker\nfrom gunicorn.dirty.protocol import (\n    DirtyProtocol,\n    BinaryProtocol,\n    make_request,\n    HEADER_SIZE,\n    HEADER_FORMAT,\n)\nfrom gunicorn.dirty.errors import DirtyAppNotFoundError\n\n\nimport struct\n\n\nclass MockLog:\n    \"\"\"Mock logger for testing.\"\"\"\n\n    def __init__(self):\n        self.messages = []\n\n    def debug(self, msg, *args):\n        self.messages.append((\"debug\", msg % args if args else msg))\n\n    def info(self, msg, *args):\n        self.messages.append((\"info\", msg % args if args else msg))\n\n    def warning(self, msg, *args):\n        self.messages.append((\"warning\", msg % args if args else msg))\n\n    def error(self, msg, *args):\n        self.messages.append((\"error\", msg % args if args else msg))\n\n    def close_on_exec(self):\n        pass\n\n    def reopen_files(self):\n        pass\n\n\nclass MockStreamWriter:\n    \"\"\"Mock StreamWriter that captures written messages.\"\"\"\n\n    def __init__(self):\n        self.messages = []\n        self._buffer = b\"\"\n        self.closed = False\n\n    def write(self, data):\n        self._buffer += data\n\n    async def drain(self):\n        # Decode the buffer to extract messages using binary protocol\n        while len(self._buffer) >= HEADER_SIZE:\n            # Decode header to get payload length\n            _, _, length = BinaryProtocol.decode_header(\n                self._buffer[:HEADER_SIZE]\n            )\n            total_size = HEADER_SIZE + length\n            if len(self._buffer) >= total_size:\n                msg_data = self._buffer[:total_size]\n                self._buffer = self._buffer[total_size:]\n                # decode_message returns (msg_type_str, request_id, payload_dict)\n                msg_type_str, request_id, payload_dict = BinaryProtocol.decode_message(msg_data)\n                # Reconstruct the dict format for backwards compatibility\n                result = {\"type\": msg_type_str, \"id\": request_id}\n                result.update(payload_dict)\n                self.messages.append(result)\n            else:\n                break\n\n    def close(self):\n        self.closed = True\n\n    async def wait_closed(self):\n        pass\n\n    def get_extra_info(self, name):\n        return None\n\n\nclass TestDirtyWorkerInit:\n    \"\"\"Tests for DirtyWorker initialization.\"\"\"\n\n    def test_init_attributes(self):\n        \"\"\"Test that worker is initialized with correct attributes.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[\"tests.support_dirty_app:TestDirtyApp\"],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            assert worker.age == 1\n            assert worker.ppid == os.getpid()\n            assert worker.app_paths == [\"tests.support_dirty_app:TestDirtyApp\"]\n            assert worker.socket_path == socket_path\n            assert worker.booted is False\n            assert worker.alive is True\n            assert worker.apps == {}\n\n    def test_str_representation(self):\n        \"\"\"Test string representation.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            assert \"DirtyWorker\" in str(worker)\n\n\nclass TestDirtyWorkerLoadApps:\n    \"\"\"Tests for app loading.\"\"\"\n\n    def test_load_apps_success(self):\n        \"\"\"Test successful app loading.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[\"tests.support_dirty_app:TestDirtyApp\"],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            worker.load_apps()\n\n            assert \"tests.support_dirty_app:TestDirtyApp\" in worker.apps\n            app = worker.apps[\"tests.support_dirty_app:TestDirtyApp\"]\n            assert app.initialized is True  # init() was called\n\n    def test_load_apps_failure(self):\n        \"\"\"Test failed app loading.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[\"nonexistent:App\"],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            with pytest.raises(Exception):\n                worker.load_apps()\n\n\nclass TestDirtyWorkerExecute:\n    \"\"\"Tests for request execution.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_execute_success(self):\n        \"\"\"Test successful execution.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[\"tests.support_dirty_app:TestDirtyApp\"],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            worker.load_apps()\n\n            result = await worker.execute(\n                \"tests.support_dirty_app:TestDirtyApp\",\n                \"compute\",\n                [2, 3],\n                {\"operation\": \"add\"}\n            )\n\n            assert result == 5\n\n    @pytest.mark.asyncio\n    async def test_execute_app_not_found(self):\n        \"\"\"Test execution with unknown app.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            with pytest.raises(DirtyAppNotFoundError):\n                await worker.execute(\"unknown:App\", \"action\", [], {})\n\n\nclass TestDirtyWorkerHandleRequest:\n    \"\"\"Tests for request handling.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_handle_request_success(self):\n        \"\"\"Test handling a successful request.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[\"tests.support_dirty_app:TestDirtyApp\"],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            worker.load_apps()\n\n            request = make_request(\n                request_id=123,\n                app_path=\"tests.support_dirty_app:TestDirtyApp\",\n                action=\"compute\",\n                args=(2, 3),\n                kwargs={\"operation\": \"multiply\"}\n            )\n\n            writer = MockStreamWriter()\n            await worker.handle_request(request, writer)\n\n            assert len(writer.messages) == 1\n            response = writer.messages[0]\n            assert response[\"type\"] == DirtyProtocol.MSG_TYPE_RESPONSE\n            assert response[\"id\"] == 123\n            assert response[\"result\"] == 6\n\n    @pytest.mark.asyncio\n    async def test_handle_request_error(self):\n        \"\"\"Test handling a request that fails.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[\"tests.support_dirty_app:TestDirtyApp\"],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            worker.load_apps()\n\n            request = make_request(\n                request_id=456,\n                app_path=\"tests.support_dirty_app:TestDirtyApp\",\n                action=\"compute\",\n                args=(2, 3),\n                kwargs={\"operation\": \"invalid\"}\n            )\n\n            writer = MockStreamWriter()\n            await worker.handle_request(request, writer)\n\n            assert len(writer.messages) == 1\n            response = writer.messages[0]\n            assert response[\"type\"] == DirtyProtocol.MSG_TYPE_ERROR\n            assert response[\"id\"] == 456\n            assert \"Unknown operation\" in response[\"error\"][\"message\"]\n\n    @pytest.mark.asyncio\n    async def test_handle_request_unknown_type(self):\n        \"\"\"Test handling request with unknown type.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            request = {\"type\": \"unknown\", \"id\": 789}\n            writer = MockStreamWriter()\n            await worker.handle_request(request, writer)\n\n            assert len(writer.messages) == 1\n            response = writer.messages[0]\n            assert response[\"type\"] == DirtyProtocol.MSG_TYPE_ERROR\n            assert \"Unknown message type\" in response[\"error\"][\"message\"]\n\n\nclass TestDirtyWorkerCleanup:\n    \"\"\"Tests for worker cleanup.\"\"\"\n\n    def test_cleanup_closes_apps(self):\n        \"\"\"Test that cleanup closes all apps.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[\"tests.support_dirty_app:TestDirtyApp\"],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            worker.load_apps()\n            app = worker.apps[\"tests.support_dirty_app:TestDirtyApp\"]\n            assert app.closed is False\n\n            worker._cleanup()\n            assert app.closed is True\n\n    def test_cleanup_removes_socket(self):\n        \"\"\"Test that cleanup removes the socket file.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            # Create the socket file\n            with open(socket_path, 'w') as f:\n                f.write('')\n\n            assert os.path.exists(socket_path)\n            worker._cleanup()\n            assert not os.path.exists(socket_path)\n\n\nclass TestDirtyWorkerNotify:\n    \"\"\"Tests for worker heartbeat.\"\"\"\n\n    def test_notify_calls_tmp_notify(self):\n        \"\"\"Test that notify calls tmp.notify().\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            # Just verify notify doesn't raise\n            worker.notify()\n            worker.notify()\n\n            worker.tmp.close()\n\n\nclass TestDirtyWorkerSignals:\n    \"\"\"Tests for signal handling.\"\"\"\n\n    def test_signal_handler_sets_alive_false(self):\n        \"\"\"Test that signal handler sets alive to False.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            assert worker.alive is True\n            worker._signal_handler(signal.SIGTERM, None)\n            assert worker.alive is False\n\n            worker.tmp.close()\n\n    def test_signal_handler_sigusr1_reopens_logs(self):\n        \"\"\"Test that SIGUSR1 calls reopen_files.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            # Should call reopen_files and NOT set alive to False\n            assert worker.alive is True\n            worker._signal_handler(signal.SIGUSR1, None)\n            assert worker.alive is True\n\n            worker.tmp.close()\n\n    def test_signal_handler_with_loop_calls_shutdown(self):\n        \"\"\"Test that signal handler with loop calls shutdown.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            # Create a mock loop\n            loop = asyncio.new_event_loop()\n            worker._loop = loop\n            shutdown_called = []\n\n            def mock_call_soon_threadsafe(cb):\n                shutdown_called.append(cb)\n\n            loop.call_soon_threadsafe = mock_call_soon_threadsafe\n\n            worker._signal_handler(signal.SIGTERM, None)\n            assert worker.alive is False\n            assert len(shutdown_called) == 1\n\n            loop.close()\n            worker.tmp.close()\n\n    def test_signal_handler_sigquit(self):\n        \"\"\"Test SIGQUIT handling.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            worker._signal_handler(signal.SIGQUIT, None)\n            assert worker.alive is False\n\n            worker.tmp.close()\n\n    def test_signal_handler_sigint(self):\n        \"\"\"Test SIGINT handling.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            worker._signal_handler(signal.SIGINT, None)\n            assert worker.alive is False\n\n            worker.tmp.close()\n\n    def test_signal_handler_sigabrt(self):\n        \"\"\"Test SIGABRT handling (timeout signal).\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            worker._signal_handler(signal.SIGABRT, None)\n            assert worker.alive is False\n\n            worker.tmp.close()\n\n\nclass TestDirtyWorkerShutdown:\n    \"\"\"Tests for worker shutdown.\"\"\"\n\n    def test_shutdown_closes_server(self):\n        \"\"\"Test that _shutdown closes the server.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            # Mock server\n            class MockServer:\n                def __init__(self):\n                    self.closed = False\n\n                def close(self):\n                    self.closed = True\n\n            worker._server = MockServer()\n            worker._shutdown()\n            assert worker._server.closed is True\n\n            worker.tmp.close()\n\n    def test_shutdown_without_server(self):\n        \"\"\"Test that _shutdown works when server is None.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            # Should not raise\n            worker._shutdown()\n\n            worker.tmp.close()\n\n\nclass TestDirtyWorkerRunAsync:\n    \"\"\"Tests for async run loop.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_run_async_creates_socket(self):\n        \"\"\"Test that _run_async creates Unix socket server.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_timeout\", 300)\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n            worker.pid = os.getpid()\n\n            # Start the server in background\n            async def run_briefly():\n                # Remove existing socket\n                if os.path.exists(socket_path):\n                    os.unlink(socket_path)\n\n                worker._server = await asyncio.start_unix_server(\n                    worker.handle_connection,\n                    path=socket_path\n                )\n                os.chmod(socket_path, 0o600)\n\n                # Verify socket exists\n                assert os.path.exists(socket_path)\n\n                # Close immediately\n                worker._server.close()\n                await worker._server.wait_closed()\n\n            await run_briefly()\n\n            worker.tmp.close()\n\n    @pytest.mark.asyncio\n    async def test_heartbeat_loop(self):\n        \"\"\"Test heartbeat loop updates tmp.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_timeout\", 300)\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            # Test that notify method works\n            worker.notify()\n            worker.notify()\n            worker.notify()\n\n            # Verify no exceptions raised\n            assert worker.tmp is not None\n\n            worker.tmp.close()\n\n    @pytest.mark.asyncio\n    async def test_handle_connection_basic(self):\n        \"\"\"Test handle_connection reads and responds to messages.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[\"tests.support_dirty_app:TestDirtyApp\"],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            worker.load_apps()\n            worker.pid = os.getpid()\n\n            # Create a simple test using stream reader/writer\n            request = make_request(\n                request_id=999,\n                app_path=\"tests.support_dirty_app:TestDirtyApp\",\n                action=\"compute\",\n                args=(5, 3),\n                kwargs={\"operation\": \"add\"}\n            )\n\n            # Mock reader and writer\n            reader = asyncio.StreamReader()\n            encoded_request = BinaryProtocol._encode_from_dict(request)\n            reader.feed_data(encoded_request)\n            reader.feed_eof()\n\n            writer = MockStreamWriter()\n\n            # Handle one message then exit\n            worker.alive = True\n            try:\n                message = await DirtyProtocol.read_message_async(reader)\n                await worker.handle_request(message, writer)\n            except asyncio.IncompleteReadError:\n                pass\n\n            # Check response from writer\n            assert len(writer.messages) == 1\n            response = writer.messages[0]\n            assert response[\"type\"] == DirtyProtocol.MSG_TYPE_RESPONSE\n            assert response[\"result\"] == 8\n\n            worker._cleanup()\n\n\nclass TestDirtyWorkerRun:\n    \"\"\"Tests for the run() method.\"\"\"\n\n    def test_run_creates_and_runs_loop(self):\n        \"\"\"Test that run() creates and runs an event loop.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_timeout\", 300)\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n            worker.pid = os.getpid()\n\n            # Override _run_async to exit quickly\n            run_async_called = []\n\n            async def mock_run_async():\n                run_async_called.append(True)\n                # Exit immediately\n\n            worker._run_async = mock_run_async\n\n            worker.run()\n\n            assert len(run_async_called) == 1\n\n            worker.tmp.close()\n\n    def test_run_handles_exception(self):\n        \"\"\"Test that run() handles exceptions and cleans up.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n            worker.pid = os.getpid()\n\n            # Override _run_async to raise\n            async def failing_run_async():\n                raise RuntimeError(\"Test error\")\n\n            worker._run_async = failing_run_async\n\n            # Should not raise, should log error\n            worker.run()\n\n            # Check error was logged\n            assert any(\"Worker error\" in msg for level, msg in log.messages)\n\n\nclass TestDirtyWorkerInitProcess:\n    \"\"\"Tests for init_process post-fork setup.\"\"\"\n\n    def test_init_signals_setup(self):\n        \"\"\"Test that init_signals sets up signal handlers.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            # Store original handlers\n            original_sigterm = signal.getsignal(signal.SIGTERM)\n\n            try:\n                worker.init_signals()\n\n                # Verify handlers are set\n                assert signal.getsignal(signal.SIGTERM) == worker._signal_handler\n                assert signal.getsignal(signal.SIGQUIT) == worker._signal_handler\n                assert signal.getsignal(signal.SIGINT) == worker._signal_handler\n                assert signal.getsignal(signal.SIGABRT) == worker._signal_handler\n                assert signal.getsignal(signal.SIGUSR1) == worker._signal_handler\n            finally:\n                # Restore original handler\n                signal.signal(signal.SIGTERM, original_sigterm)\n\n            worker.tmp.close()\n\n\nclass TestDirtyWorkerCleanupErrors:\n    \"\"\"Tests for cleanup error handling.\"\"\"\n\n    def test_cleanup_handles_app_close_error(self):\n        \"\"\"Test that cleanup handles errors when closing apps.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[\"tests.support_dirty_app:TestDirtyApp\"],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            worker.load_apps()\n            app = worker.apps[\"tests.support_dirty_app:TestDirtyApp\"]\n\n            # Make close() raise an error\n            def failing_close():\n                raise RuntimeError(\"Close failed\")\n\n            app.close = failing_close\n\n            # Should not raise, should log error\n            worker._cleanup()\n\n            assert any(\"Error closing dirty app\" in msg for level, msg in log.messages)\n\n    def test_cleanup_handles_missing_socket(self):\n        \"\"\"Test that cleanup handles non-existent socket file.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"nonexistent.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            # Should not raise even if socket doesn't exist\n            worker._cleanup()\n\n    def test_cleanup_handles_tmp_close_error(self):\n        \"\"\"Test that cleanup handles tmp.close() errors.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            # Close tmp so second close might fail\n            worker.tmp.close()\n\n            # Should not raise\n            worker._cleanup()\n\n\nclass TestDirtyWorkerLoadAppsInit:\n    \"\"\"Tests for app loading with init failure.\"\"\"\n\n    def test_load_apps_init_failure(self):\n        \"\"\"Test that load_apps handles init() failure.\"\"\"\n        cfg = Config()\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[\"tests.support_dirty_app:BrokenInitApp\"],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n\n            with pytest.raises(RuntimeError, match=\"Init failed\"):\n                worker.load_apps()\n\n            # Error should be logged\n            assert any(\"Failed to initialize\" in msg for level, msg in log.messages)\n\n\nclass TestDirtyWorkerExecutionTimeout:\n    \"\"\"Tests for execution timeout control.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_execute_with_timeout(self):\n        \"\"\"Test that execute enforces timeout.\"\"\"\n        from concurrent.futures import ThreadPoolExecutor\n\n        cfg = Config()\n        cfg.set(\"dirty_timeout\", 1)  # 1 second timeout\n        cfg.set(\"dirty_threads\", 1)\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[\"tests.support_dirty_app:SlowDirtyApp\"],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n            worker.pid = os.getpid()\n\n            # Create executor manually for test\n            worker._executor = ThreadPoolExecutor(max_workers=1)\n\n            try:\n                worker.load_apps()\n\n                # Execute slow action that exceeds timeout\n                from gunicorn.dirty.errors import DirtyTimeoutError\n                with pytest.raises(DirtyTimeoutError):\n                    await worker.execute(\n                        \"tests.support_dirty_app:SlowDirtyApp\",\n                        \"slow_action\",\n                        [],\n                        {\"delay\": 5.0}  # 5 second delay, 1 second timeout\n                    )\n            finally:\n                worker._cleanup()\n\n    @pytest.mark.asyncio\n    async def test_execute_within_timeout(self):\n        \"\"\"Test that execute succeeds within timeout.\"\"\"\n        from concurrent.futures import ThreadPoolExecutor\n\n        cfg = Config()\n        cfg.set(\"dirty_timeout\", 10)  # 10 second timeout\n        cfg.set(\"dirty_threads\", 1)\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[\"tests.support_dirty_app:SlowDirtyApp\"],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n            worker.pid = os.getpid()\n\n            # Create executor manually for test\n            worker._executor = ThreadPoolExecutor(max_workers=1)\n\n            try:\n                worker.load_apps()\n\n                # Execute fast action that completes within timeout\n                result = await worker.execute(\n                    \"tests.support_dirty_app:SlowDirtyApp\",\n                    \"fast_action\",\n                    [],\n                    {}\n                )\n                assert result == {\"fast\": True}\n            finally:\n                worker._cleanup()\n\n    @pytest.mark.asyncio\n    async def test_execute_no_timeout_when_zero(self):\n        \"\"\"Test that timeout is disabled when dirty_timeout is 0.\"\"\"\n        from concurrent.futures import ThreadPoolExecutor\n\n        cfg = Config()\n        cfg.set(\"dirty_timeout\", 0)  # Disabled\n        cfg.set(\"dirty_threads\", 1)\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[\"tests.support_dirty_app:TestDirtyApp\"],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n            worker.pid = os.getpid()\n\n            # Create executor manually for test\n            worker._executor = ThreadPoolExecutor(max_workers=1)\n\n            try:\n                worker.load_apps()\n\n                # Should work with no timeout\n                result = await worker.execute(\n                    \"tests.support_dirty_app:TestDirtyApp\",\n                    \"compute\",\n                    [2, 3],\n                    {\"operation\": \"add\"}\n                )\n                assert result == 5\n            finally:\n                worker._cleanup()\n\n    def test_run_creates_executor_with_threads(self):\n        \"\"\"Test that run() creates executor with dirty_threads config.\"\"\"\n        cfg = Config()\n        cfg.set(\"dirty_timeout\", 300)\n        cfg.set(\"dirty_threads\", 4)\n        log = MockLog()\n\n        with tempfile.TemporaryDirectory() as tmpdir:\n            socket_path = os.path.join(tmpdir, \"worker.sock\")\n            worker = DirtyWorker(\n                age=1,\n                ppid=os.getpid(),\n                app_paths=[\"tests.support_dirty_app:TestDirtyApp\"],\n                cfg=cfg,\n                log=log,\n                socket_path=socket_path\n            )\n            worker.pid = os.getpid()\n            worker.load_apps()\n\n            # Simulate what run() does\n            from concurrent.futures import ThreadPoolExecutor\n            worker._executor = ThreadPoolExecutor(\n                max_workers=cfg.dirty_threads,\n                thread_name_prefix=f\"dirty-worker-{worker.pid}-\"\n            )\n\n            assert worker._executor._max_workers == 4\n\n            worker._cleanup()\n            assert worker._executor is None\n"
  },
  {
    "path": "tests/test_early_hints.py",
    "content": "# -*- coding: utf-8 -\n#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Tests for HTTP 103 Early Hints support (RFC 8297).\"\"\"\n\nimport pytest\nfrom unittest import mock\nfrom io import BytesIO\n\n# Check if h2 is available for HTTP/2 tests\ntry:\n    import h2.connection\n    import h2.config\n    import h2.events\n    H2_AVAILABLE = True\nexcept ImportError:\n    H2_AVAILABLE = False\n\nfrom gunicorn.http import wsgi\n\n\nclass MockConfig:\n    \"\"\"Mock gunicorn configuration.\"\"\"\n\n    def __init__(self):\n        self.is_ssl = False\n        self.workers = 1\n        self.limit_request_fields = 100\n        self.limit_request_field_size = 8190\n        self.limit_request_line = 8190\n        self.secure_scheme_headers = {}\n        self.forwarded_allow_ips = ['127.0.0.1']\n        self.forwarder_headers = []\n        self.strip_header_spaces = False\n        self.permit_obsolete_folding = False\n        self.header_map = \"refuse\"\n        self.sendfile = True\n        self.errorlog = \"-\"\n\n        # HTTP/2 settings\n        self.http2_max_concurrent_streams = 100\n        self.http2_initial_window_size = 65535\n        self.http2_max_frame_size = 16384\n        self.http2_max_header_list_size = 65536\n\n    def forwarded_allow_networks(self):\n        return []\n\n\nclass MockRequest:\n    \"\"\"Mock HTTP request for testing.\"\"\"\n\n    def __init__(self, version=(1, 1)):\n        self.version = version\n        self.method = \"GET\"\n        self.uri = \"/\"\n        self.path = \"/\"\n        self.query = \"\"\n        self.fragment = \"\"\n        self.scheme = \"http\"\n        self.headers = []\n        self.body = BytesIO(b\"\")\n        self.proxy_protocol_info = None\n        self._expected_100_continue = False\n\n    def should_close(self):\n        return False\n\n\nclass MockSocket:\n    \"\"\"Mock socket for testing.\"\"\"\n\n    def __init__(self):\n        self._sent = bytearray()\n        self._closed = False\n\n    def sendall(self, data):\n        if self._closed:\n            raise OSError(\"Socket is closed\")\n        self._sent.extend(data)\n\n    def send(self, data):\n        if self._closed:\n            raise OSError(\"Socket is closed\")\n        self._sent.extend(data)\n        return len(data)\n\n    def get_sent_data(self):\n        return bytes(self._sent)\n\n    def clear(self):\n        self._sent = bytearray()\n\n    def close(self):\n        self._closed = True\n\n\nclass TestWSGIEarlyHints:\n    \"\"\"Test WSGI wsgi.early_hints callback.\"\"\"\n\n    def test_early_hints_callback_in_environ(self):\n        \"\"\"Verify wsgi.early_hints is added to environ.\"\"\"\n        cfg = MockConfig()\n        req = MockRequest()\n        sock = MockSocket()\n\n        resp, environ = wsgi.create(req, sock, ('127.0.0.1', 12345),\n                                    ('127.0.0.1', 8000), cfg)\n\n        assert 'wsgi.early_hints' in environ\n        assert callable(environ['wsgi.early_hints'])\n\n    def test_send_single_early_hint(self):\n        \"\"\"Test sending one Link header as early hint.\"\"\"\n        cfg = MockConfig()\n        req = MockRequest(version=(1, 1))\n        sock = MockSocket()\n\n        resp, environ = wsgi.create(req, sock, ('127.0.0.1', 12345),\n                                    ('127.0.0.1', 8000), cfg)\n\n        # Send early hints\n        environ['wsgi.early_hints']([\n            ('Link', '</style.css>; rel=preload; as=style'),\n        ])\n\n        sent_data = sock.get_sent_data()\n        assert b\"HTTP/1.1 103 Early Hints\\r\\n\" in sent_data\n        assert b\"Link: </style.css>; rel=preload; as=style\\r\\n\" in sent_data\n        assert sent_data.endswith(b\"\\r\\n\\r\\n\")\n\n    def test_send_multiple_early_hints(self):\n        \"\"\"Test sending multiple Link headers.\"\"\"\n        cfg = MockConfig()\n        req = MockRequest(version=(1, 1))\n        sock = MockSocket()\n\n        resp, environ = wsgi.create(req, sock, ('127.0.0.1', 12345),\n                                    ('127.0.0.1', 8000), cfg)\n\n        environ['wsgi.early_hints']([\n            ('Link', '</style.css>; rel=preload; as=style'),\n            ('Link', '</app.js>; rel=preload; as=script'),\n        ])\n\n        sent_data = sock.get_sent_data()\n        assert b\"HTTP/1.1 103 Early Hints\\r\\n\" in sent_data\n        assert b\"Link: </style.css>; rel=preload; as=style\\r\\n\" in sent_data\n        assert b\"Link: </app.js>; rel=preload; as=script\\r\\n\" in sent_data\n\n    def test_early_hints_not_sent_for_http10(self):\n        \"\"\"Test that early hints are not sent for HTTP/1.0 clients.\"\"\"\n        cfg = MockConfig()\n        req = MockRequest(version=(1, 0))  # HTTP/1.0\n        sock = MockSocket()\n\n        resp, environ = wsgi.create(req, sock, ('127.0.0.1', 12345),\n                                    ('127.0.0.1', 8000), cfg)\n\n        # Try to send early hints\n        environ['wsgi.early_hints']([\n            ('Link', '</style.css>; rel=preload; as=style'),\n        ])\n\n        # Nothing should be sent for HTTP/1.0\n        sent_data = sock.get_sent_data()\n        assert sent_data == b\"\"\n\n    def test_multiple_early_hints_calls(self):\n        \"\"\"Test multiple calls to wsgi.early_hints (multiple 103 responses).\"\"\"\n        cfg = MockConfig()\n        req = MockRequest(version=(1, 1))\n        sock = MockSocket()\n\n        resp, environ = wsgi.create(req, sock, ('127.0.0.1', 12345),\n                                    ('127.0.0.1', 8000), cfg)\n\n        # First early hints call\n        environ['wsgi.early_hints']([\n            ('Link', '</critical.css>; rel=preload; as=style'),\n        ])\n\n        # Second early hints call\n        environ['wsgi.early_hints']([\n            ('Link', '</app.js>; rel=preload; as=script'),\n        ])\n\n        sent_data = sock.get_sent_data()\n        # Should have two separate 103 responses\n        assert sent_data.count(b\"HTTP/1.1 103 Early Hints\\r\\n\") == 2\n\n    def test_early_hints_with_bytes_headers(self):\n        \"\"\"Test early hints with bytes header values.\"\"\"\n        cfg = MockConfig()\n        req = MockRequest(version=(1, 1))\n        sock = MockSocket()\n\n        resp, environ = wsgi.create(req, sock, ('127.0.0.1', 12345),\n                                    ('127.0.0.1', 8000), cfg)\n\n        # Send with bytes values\n        environ['wsgi.early_hints']([\n            (b'Link', b'</style.css>; rel=preload; as=style'),\n        ])\n\n        sent_data = sock.get_sent_data()\n        assert b\"HTTP/1.1 103 Early Hints\\r\\n\" in sent_data\n        assert b\"Link: </style.css>; rel=preload; as=style\\r\\n\" in sent_data\n\n    def test_empty_early_hints(self):\n        \"\"\"Test early hints with empty headers list.\"\"\"\n        cfg = MockConfig()\n        req = MockRequest(version=(1, 1))\n        sock = MockSocket()\n\n        resp, environ = wsgi.create(req, sock, ('127.0.0.1', 12345),\n                                    ('127.0.0.1', 8000), cfg)\n\n        # Send empty headers\n        environ['wsgi.early_hints']([])\n\n        sent_data = sock.get_sent_data()\n        # Should still send 103 response with no headers\n        assert sent_data == b\"HTTP/1.1 103 Early Hints\\r\\n\\r\\n\"\n\n\n@pytest.mark.skipif(not H2_AVAILABLE, reason=\"h2 library not available\")\nclass TestHTTP2EarlyHints:\n    \"\"\"Test HTTP/2 early hints (send_informational method).\"\"\"\n\n    def _create_mock_http2_config(self):\n        \"\"\"Create mock config for HTTP/2.\"\"\"\n        cfg = MockConfig()\n        return cfg\n\n    def _create_mock_socket(self):\n        \"\"\"Create mock socket for HTTP/2.\"\"\"\n        return MockSocket()\n\n    def test_send_informational_method_exists(self):\n        \"\"\"Test that send_informational method exists on HTTP2ServerConnection.\"\"\"\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = self._create_mock_http2_config()\n        sock = self._create_mock_socket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n\n        assert hasattr(conn, 'send_informational')\n        assert callable(conn.send_informational)\n\n    def test_send_informational_invalid_status(self):\n        \"\"\"Test send_informational raises for non-1xx status.\"\"\"\n        from gunicorn.http2.connection import HTTP2ServerConnection\n        from gunicorn.http2.errors import HTTP2Error\n\n        cfg = self._create_mock_http2_config()\n        sock = self._create_mock_socket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        conn.initiate_connection()\n\n        # Need to create a stream first\n        client_conn = h2.connection.H2Connection(\n            config=h2.config.H2Configuration(client_side=True)\n        )\n        client_conn.initiate_connection()\n\n        # Get client's initial data\n        client_data = client_conn.data_to_send()\n        conn.receive_data(client_data)\n\n        # Create a request on the client\n        client_conn.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'localhost'),\n        ], end_stream=True)\n        request_data = client_conn.data_to_send()\n        conn.receive_data(request_data)\n\n        # Try to send 200 as informational (should fail)\n        with pytest.raises(HTTP2Error) as excinfo:\n            conn.send_informational(1, 200, [('link', '</style.css>')])\n        assert \"Invalid informational status\" in str(excinfo.value)\n\n    def test_send_informational_103(self):\n        \"\"\"Test sending 103 Early Hints over HTTP/2.\"\"\"\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = self._create_mock_http2_config()\n        sock = self._create_mock_socket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        conn.initiate_connection()\n\n        # Create a client connection\n        client_conn = h2.connection.H2Connection(\n            config=h2.config.H2Configuration(client_side=True)\n        )\n        client_conn.initiate_connection()\n        client_data = client_conn.data_to_send()\n        conn.receive_data(client_data)\n\n        # Create a request on the client\n        client_conn.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'localhost'),\n        ], end_stream=True)\n        request_data = client_conn.data_to_send()\n        conn.receive_data(request_data)\n\n        # Clear sent data to isolate the informational response\n        sock.clear()\n\n        # Send 103 Early Hints\n        conn.send_informational(1, 103, [\n            ('link', '</style.css>; rel=preload; as=style'),\n        ])\n\n        # Verify data was sent\n        sent_data = sock.get_sent_data()\n        assert len(sent_data) > 0\n\n        # Feed the data back to client to verify it's valid HTTP/2\n        client_conn.receive_data(sent_data)\n        # Client should receive an informational response\n\n    def test_send_informational_stream_not_found(self):\n        \"\"\"Test send_informational raises for non-existent stream.\"\"\"\n        from gunicorn.http2.connection import HTTP2ServerConnection\n        from gunicorn.http2.errors import HTTP2Error\n\n        cfg = self._create_mock_http2_config()\n        sock = self._create_mock_socket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        conn.initiate_connection()\n\n        # Try to send on non-existent stream\n        with pytest.raises(HTTP2Error) as excinfo:\n            conn.send_informational(999, 103, [('link', '</style.css>')])\n        assert \"not found\" in str(excinfo.value)\n\n\n@pytest.mark.skipif(not H2_AVAILABLE, reason=\"h2 library not available\")\nclass TestAsyncHTTP2EarlyHints:\n    \"\"\"Test async HTTP/2 early hints.\"\"\"\n\n    def test_async_send_informational_method_exists(self):\n        \"\"\"Test that send_informational method exists on AsyncHTTP2Connection.\"\"\"\n        from gunicorn.http2.async_connection import AsyncHTTP2Connection\n\n        cfg = MockConfig()\n        reader = mock.MagicMock()\n        writer = mock.MagicMock()\n        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))\n\n        assert hasattr(conn, 'send_informational')\n        assert callable(conn.send_informational)\n\n\nclass TestASGIEarlyHints:\n    \"\"\"Test ASGI http.response.informational handling.\"\"\"\n\n    def test_reason_phrase_103(self):\n        \"\"\"Test that 103 has correct reason phrase.\"\"\"\n        from gunicorn.asgi.protocol import ASGIProtocol\n\n        worker = mock.MagicMock()\n        worker.cfg = MockConfig()\n        worker.log = mock.MagicMock()\n\n        protocol = ASGIProtocol(worker)\n        reason = protocol._get_reason_phrase(103)\n        assert reason == \"Early Hints\"\n\n    def test_reason_phrase_100(self):\n        \"\"\"Test that 100 Continue has correct reason phrase.\"\"\"\n        from gunicorn.asgi.protocol import ASGIProtocol\n\n        worker = mock.MagicMock()\n        worker.cfg = MockConfig()\n        worker.log = mock.MagicMock()\n\n        protocol = ASGIProtocol(worker)\n        reason = protocol._get_reason_phrase(100)\n        assert reason == \"Continue\"\n\n    def test_reason_phrase_101(self):\n        \"\"\"Test that 101 Switching Protocols has correct reason phrase.\"\"\"\n        from gunicorn.asgi.protocol import ASGIProtocol\n\n        worker = mock.MagicMock()\n        worker.cfg = MockConfig()\n        worker.log = mock.MagicMock()\n\n        protocol = ASGIProtocol(worker)\n        reason = protocol._get_reason_phrase(101)\n        assert reason == \"Switching Protocols\"\n"
  },
  {
    "path": "tests/test_gthread.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Tests for the gthread worker.\"\"\"\n\nimport errno\nimport fcntl\nimport os\nimport selectors\nimport threading\nimport time\nfrom collections import deque\nfrom concurrent import futures\nfrom unittest import mock\n\nimport pytest\n\nfrom gunicorn.config import Config\nfrom gunicorn.workers import gthread\n\n\nclass FakeSocket:\n    \"\"\"Mock socket for testing.\"\"\"\n\n    def __init__(self, data=b''):\n        self.data = data\n        self.closed = False\n        self.blocking = True\n        self._fileno = id(self) % 65536\n\n    def fileno(self):\n        return self._fileno\n\n    def setblocking(self, blocking):\n        self.blocking = blocking\n\n    def recv(self, size):\n        if self.closed:\n            raise OSError(errno.EBADF, \"Bad file descriptor\")\n        result = self.data[:size]\n        self.data = self.data[size:]\n        return result\n\n    def send(self, data):\n        if self.closed:\n            raise OSError(errno.EPIPE, \"Broken pipe\")\n        return len(data)\n\n    def close(self):\n        self.closed = True\n\n    def getsockname(self):\n        return ('127.0.0.1', 8000)\n\n    def getpeername(self):\n        return ('127.0.0.1', 12345)\n\n\nclass TestTConn:\n    \"\"\"Tests for TConn connection wrapper.\"\"\"\n\n    def test_tconn_init(self):\n        \"\"\"Test TConn initialization.\"\"\"\n        cfg = Config()\n        sock = FakeSocket()\n        client = ('127.0.0.1', 12345)\n        server = ('127.0.0.1', 8000)\n\n        conn = gthread.TConn(cfg, sock, client, server)\n\n        assert conn.cfg is cfg\n        assert conn.sock is sock\n        assert conn.client == client\n        assert conn.server == server\n        assert conn.timeout is None\n        assert conn.parser is None\n        assert conn.initialized is False\n\n    def test_tconn_init_sets_blocking_false(self):\n        \"\"\"Test that TConn sets socket to non-blocking initially.\"\"\"\n        cfg = Config()\n        sock = FakeSocket()\n        sock.setblocking(True)\n\n        gthread.TConn(cfg, sock, ('127.0.0.1', 12345), ('127.0.0.1', 8000))\n\n        # TConn sets socket to non-blocking in __init__\n        assert sock.blocking is False\n\n    def test_tconn_init_method_sets_blocking_true(self):\n        \"\"\"Test that conn.init() sets socket back to blocking.\"\"\"\n        cfg = Config()\n        sock = FakeSocket()\n\n        conn = gthread.TConn(cfg, sock, ('127.0.0.1', 12345), ('127.0.0.1', 8000))\n        conn.init()\n\n        assert sock.blocking is True\n        assert conn.initialized is True\n        assert conn.parser is not None\n\n    def test_tconn_set_timeout(self):\n        \"\"\"Test timeout setting using monotonic clock.\"\"\"\n        cfg = Config()\n        cfg.set('keepalive', 5)\n        sock = FakeSocket()\n\n        conn = gthread.TConn(cfg, sock, ('127.0.0.1', 12345), ('127.0.0.1', 8000))\n        before = time.monotonic()\n        conn.set_timeout()\n        after = time.monotonic()\n\n        assert conn.timeout is not None\n        assert before + 5 <= conn.timeout <= after + 5\n\n    def test_tconn_close(self):\n        \"\"\"Test connection closing.\"\"\"\n        cfg = Config()\n        sock = FakeSocket()\n\n        conn = gthread.TConn(cfg, sock, ('127.0.0.1', 12345), ('127.0.0.1', 8000))\n        conn.close()\n\n        assert sock.closed is True\n\n\nclass TestPollableMethodQueue:\n    \"\"\"Tests for PollableMethodQueue.\"\"\"\n\n    def test_queue_init_and_close(self):\n        \"\"\"Test queue initialization and cleanup.\"\"\"\n        q = gthread.PollableMethodQueue()\n        q.init()\n\n        assert q._read_fd is not None\n        assert q._write_fd is not None\n        assert q._queue is not None\n\n        q.close()\n\n    def test_queue_defer_and_run(self):\n        \"\"\"Test deferring and running callbacks.\"\"\"\n        q = gthread.PollableMethodQueue()\n        q.init()\n\n        results = []\n        q.defer(results.append, 42)\n\n        # Simulate the selector reading from the pipe\n        q.run_callbacks(None)\n\n        assert results == [42]\n        q.close()\n\n    def test_queue_multiple_callbacks(self):\n        \"\"\"Test multiple callbacks are executed in order.\"\"\"\n        q = gthread.PollableMethodQueue()\n        q.init()\n\n        results = []\n        for i in range(5):\n            q.defer(results.append, i)\n\n        q.run_callbacks(None)\n\n        assert results == [0, 1, 2, 3, 4]\n        q.close()\n\n    def test_queue_fileno_for_selector(self):\n        \"\"\"Test that fileno returns a valid fd for selector registration.\"\"\"\n        q = gthread.PollableMethodQueue()\n        q.init()\n\n        fd = q.fileno()\n        assert isinstance(fd, int)\n        assert fd >= 0\n\n        # Verify it can be used with a selector\n        sel = selectors.DefaultSelector()\n        sel.register(fd, selectors.EVENT_READ)\n        sel.unregister(fd)\n        sel.close()\n        q.close()\n\n    def test_queue_thread_safety(self):\n        \"\"\"Test that defer can be called from multiple threads.\"\"\"\n        q = gthread.PollableMethodQueue()\n        q.init()\n\n        results = []\n        lock = threading.Lock()\n\n        def add_callback(n):\n            def callback():\n                with lock:\n                    results.append(n)\n            q.defer(callback)\n\n        threads = []\n        for i in range(10):\n            t = threading.Thread(target=add_callback, args=(i,))\n            threads.append(t)\n            t.start()\n\n        for t in threads:\n            t.join()\n\n        # Drain all callbacks (pipe is non-blocking, may take multiple calls)\n        for _ in range(20):\n            q.run_callbacks(None)\n            if len(results) >= 10:\n                break\n\n        assert len(results) == 10\n        assert set(results) == set(range(10))\n        q.close()\n\n    def test_queue_nonblocking_pipe(self):\n        \"\"\"Test that pipe is non-blocking (BSD compatibility).\"\"\"\n        q = gthread.PollableMethodQueue()\n        q.init()\n\n        # Verify both ends are non-blocking\n        read_flags = fcntl.fcntl(q._read_fd, fcntl.F_GETFL)\n        write_flags = fcntl.fcntl(q._write_fd, fcntl.F_GETFL)\n        assert read_flags & os.O_NONBLOCK\n        assert write_flags & os.O_NONBLOCK\n\n        q.close()\n\n\nclass TestThreadWorker:\n    \"\"\"Tests for ThreadWorker.\"\"\"\n\n    def create_worker(self, cfg=None):\n        \"\"\"Create a worker instance for testing.\"\"\"\n        if cfg is None:\n            cfg = Config()\n        cfg.set('workers', 1)\n        cfg.set('threads', 4)\n        cfg.set('worker_connections', 1000)\n        cfg.set('keepalive', 2)\n\n        worker = gthread.ThreadWorker(\n            age=1,\n            ppid=os.getpid(),\n            sockets=[],\n            app=mock.Mock(),\n            timeout=30,\n            cfg=cfg,\n            log=mock.Mock(),\n        )\n        return worker\n\n    def test_worker_init(self):\n        \"\"\"Test worker initialization.\"\"\"\n        worker = self.create_worker()\n\n        assert worker.worker_connections == 1000\n        assert worker.max_keepalived == 1000 - 4  # connections - threads\n        assert worker.tpool is None\n        assert worker.poller is None\n        assert worker.nr_conns == 0\n        assert worker._accepting is False\n        assert isinstance(worker.keepalived_conns, deque)\n        assert isinstance(worker.method_queue, gthread.PollableMethodQueue)\n\n    def test_worker_check_config_warning(self):\n        \"\"\"Test that check_config warns when keepalive impossible.\"\"\"\n        cfg = Config()\n        cfg.set('worker_connections', 4)\n        cfg.set('threads', 4)\n        cfg.set('keepalive', 2)\n        log = mock.Mock()\n\n        gthread.ThreadWorker.check_config(cfg, log)\n\n        log.warning.assert_called()\n\n    def test_worker_check_config_no_warning(self):\n        \"\"\"Test that check_config doesn't warn with valid config.\"\"\"\n        cfg = Config()\n        cfg.set('worker_connections', 100)\n        cfg.set('threads', 4)\n        cfg.set('keepalive', 2)\n        log = mock.Mock()\n\n        gthread.ThreadWorker.check_config(cfg, log)\n\n        log.warning.assert_not_called()\n\n    def test_worker_init_process(self):\n        \"\"\"Test worker process initialization.\"\"\"\n        worker = self.create_worker()\n        worker.tmp = mock.Mock()\n        worker.log = mock.Mock()\n\n        # Mock super().init_process() to avoid full initialization\n        with mock.patch.object(gthread.base.Worker, 'init_process'):\n            worker.init_process()\n\n        assert worker.tpool is not None\n        assert worker.poller is not None\n        assert worker.method_queue._queue is not None\n\n        # Cleanup\n        worker.tpool.shutdown(wait=False)\n        worker.poller.close()\n        worker.method_queue.close()\n\n    def test_worker_get_thread_pool(self):\n        \"\"\"Test thread pool creation.\"\"\"\n        worker = self.create_worker()\n\n        pool = worker.get_thread_pool()\n\n        assert isinstance(pool, futures.ThreadPoolExecutor)\n        pool.shutdown(wait=False)\n\n    def test_worker_murder_keepalived(self):\n        \"\"\"Test that expired keepalive connections are cleaned up.\"\"\"\n        worker = self.create_worker()\n        worker.poller = selectors.DefaultSelector()\n\n        # Create an expired connection (using monotonic to match implementation)\n        cfg = Config()\n        sock = FakeSocket()\n        conn = gthread.TConn(cfg, sock, ('127.0.0.1', 12345), ('127.0.0.1', 8000))\n        conn.timeout = time.monotonic() - 10  # Expired 10 seconds ago\n\n        worker.keepalived_conns.append(conn)\n        worker.nr_conns = 1\n\n        # Register with poller (so it can be unregistered)\n        try:\n            with mock.patch.object(worker.poller, 'unregister'):\n                worker.murder_keepalived()\n        except (OSError, ValueError):\n            pass  # Expected with fake socket\n\n        # Connection should have been removed\n        assert len(worker.keepalived_conns) == 0\n        assert sock.closed is True\n\n        worker.poller.close()\n\n    def test_worker_is_parent_alive(self):\n        \"\"\"Test parent process check.\"\"\"\n        worker = self.create_worker()\n\n        # With correct ppid\n        worker.ppid = os.getppid()\n        assert worker.is_parent_alive() is True\n\n        # With wrong ppid\n        worker.ppid = -1\n        assert worker.is_parent_alive() is False\n\n    def test_worker_set_accept_enabled(self):\n        \"\"\"Test enabling and disabling connection acceptance.\"\"\"\n        worker = self.create_worker()\n        worker.poller = mock.Mock()\n\n        # Create a mock socket\n        mock_sock = mock.Mock()\n        mock_sock.getsockname.return_value = ('127.0.0.1', 8000)\n        worker.sockets = [mock_sock]\n\n        # Initially not accepting\n        assert worker._accepting is False\n\n        # Enable accepting\n        worker.set_accept_enabled(True)\n        assert worker._accepting is True\n        mock_sock.setblocking.assert_called_with(False)\n        worker.poller.register.assert_called_once()\n\n        # Disable accepting\n        worker.set_accept_enabled(False)\n        assert worker._accepting is False\n        worker.poller.unregister.assert_called_once()\n\n    def test_worker_handle_exit(self):\n        \"\"\"Test graceful shutdown signal handling.\"\"\"\n        worker = self.create_worker()\n        worker.method_queue.init()\n        worker.alive = True\n\n        worker.handle_exit(None, None)\n\n        assert worker.alive is False\n        worker.method_queue.close()\n\n    def test_worker_wait_for_events(self):\n        \"\"\"Test event waiting with dispatch.\"\"\"\n        worker = self.create_worker()\n        worker.poller = mock.Mock()\n\n        # Simulate an event\n        mock_key = mock.Mock()\n        callback = mock.Mock()\n        mock_key.data = callback\n        mock_key.fileobj = mock.Mock()\n        worker.poller.select.return_value = [(mock_key, None)]\n\n        worker.wait_for_and_dispatch_events(1.0)\n\n        worker.poller.select.assert_called_once_with(1.0)\n        callback.assert_called_once_with(mock_key.fileobj)\n\n\nclass TestFinishRequest:\n    \"\"\"Tests for finish_request handling.\"\"\"\n\n    def create_worker(self):\n        \"\"\"Create a worker for testing.\"\"\"\n        cfg = Config()\n        cfg.set('workers', 1)\n        cfg.set('threads', 4)\n        cfg.set('worker_connections', 1000)\n\n        worker = gthread.ThreadWorker(\n            age=1,\n            ppid=os.getpid(),\n            sockets=[],\n            app=mock.Mock(),\n            timeout=30,\n            cfg=cfg,\n            log=mock.Mock(),\n        )\n        worker.poller = mock.Mock()\n        worker.alive = True\n        return worker\n\n    def test_finish_request_cancelled(self):\n        \"\"\"Test handling of cancelled future.\"\"\"\n        worker = self.create_worker()\n        worker.nr_conns = 1\n\n        conn = mock.Mock()\n        fs = mock.Mock()\n        fs.cancelled.return_value = True\n\n        worker.finish_request(conn, fs)\n\n        assert worker.nr_conns == 0\n        conn.close.assert_called_once()\n\n    def test_finish_request_keepalive(self):\n        \"\"\"Test handling of keepalive response.\"\"\"\n        worker = self.create_worker()\n        worker.nr_conns = 1\n\n        conn = mock.Mock()\n        conn.sock = mock.Mock()\n        fs = mock.Mock()\n        fs.cancelled.return_value = False\n        fs.result.return_value = True  # keepalive=True\n\n        worker.finish_request(conn, fs)\n\n        assert worker.nr_conns == 1  # Connection kept\n        assert conn in worker.keepalived_conns\n        conn.set_timeout.assert_called_once()\n        worker.poller.register.assert_called_once()\n\n    def test_finish_request_close(self):\n        \"\"\"Test handling of non-keepalive response.\"\"\"\n        worker = self.create_worker()\n        worker.nr_conns = 1\n\n        conn = mock.Mock()\n        fs = mock.Mock()\n        fs.cancelled.return_value = False\n        fs.result.return_value = False  # keepalive=False\n\n        worker.finish_request(conn, fs)\n\n        assert worker.nr_conns == 0\n        conn.close.assert_called_once()\n\n    def test_finish_request_exception(self):\n        \"\"\"Test handling of exception in request.\"\"\"\n        worker = self.create_worker()\n        worker.nr_conns = 1\n\n        conn = mock.Mock()\n        fs = mock.Mock()\n        fs.cancelled.return_value = False\n        fs.result.side_effect = Exception(\"Test error\")\n\n        worker.finish_request(conn, fs)\n\n        assert worker.nr_conns == 0\n        conn.close.assert_called_once()\n\n\nclass TestAccept:\n    \"\"\"Tests for connection acceptance.\"\"\"\n\n    def create_worker(self):\n        \"\"\"Create a worker for testing.\"\"\"\n        cfg = Config()\n        cfg.set('workers', 1)\n        cfg.set('threads', 4)\n        cfg.set('worker_connections', 1000)\n\n        worker = gthread.ThreadWorker(\n            age=1,\n            ppid=os.getpid(),\n            sockets=[],\n            app=mock.Mock(),\n            timeout=30,\n            cfg=cfg,\n            log=mock.Mock(),\n        )\n        worker.poller = mock.Mock()\n        worker.tpool = mock.Mock()\n        worker.method_queue = mock.Mock()\n        return worker\n\n    def test_accept_success(self):\n        \"\"\"Test successful connection acceptance.\"\"\"\n        worker = self.create_worker()\n        worker.nr_conns = 0\n\n        client_sock = FakeSocket()\n        client_addr = ('127.0.0.1', 12345)\n        listener = mock.Mock()\n        listener.accept.return_value = (client_sock, client_addr)\n        listener.getsockname.return_value = ('127.0.0.1', 8000)\n\n        worker.accept(listener)\n\n        assert worker.nr_conns == 1\n        worker.tpool.submit.assert_called_once()\n\n    def test_accept_eagain(self):\n        \"\"\"Test handling of EAGAIN during accept.\"\"\"\n        worker = self.create_worker()\n        worker.nr_conns = 0\n\n        listener = mock.Mock()\n        listener.accept.side_effect = OSError(errno.EAGAIN, \"Try again\")\n\n        # Should not raise\n        worker.accept(listener)\n\n        assert worker.nr_conns == 0\n\n    def test_accept_econnaborted(self):\n        \"\"\"Test handling of ECONNABORTED during accept.\"\"\"\n        worker = self.create_worker()\n        worker.nr_conns = 0\n\n        listener = mock.Mock()\n        listener.accept.side_effect = OSError(errno.ECONNABORTED, \"Connection aborted\")\n\n        # Should not raise\n        worker.accept(listener)\n\n        assert worker.nr_conns == 0\n\n\nclass TestGracefulShutdown:\n    \"\"\"Tests for graceful shutdown behavior.\"\"\"\n\n    def create_worker(self):\n        \"\"\"Create a worker for testing.\"\"\"\n        cfg = Config()\n        cfg.set('workers', 1)\n        cfg.set('threads', 4)\n        cfg.set('worker_connections', 1000)\n        cfg.set('graceful_timeout', 5)\n\n        worker = gthread.ThreadWorker(\n            age=1,\n            ppid=os.getpid(),\n            sockets=[],\n            app=mock.Mock(),\n            timeout=30,\n            cfg=cfg,\n            log=mock.Mock(),\n        )\n        return worker\n\n    def test_handle_exit_sets_alive_false(self):\n        \"\"\"Test that handle_exit begins graceful shutdown.\"\"\"\n        worker = self.create_worker()\n        worker.method_queue.init()\n        worker.alive = True\n\n        worker.handle_exit(None, None)\n\n        assert worker.alive is False\n        worker.method_queue.close()\n\n    def test_connection_tracking(self):\n        \"\"\"Test that connection count is properly tracked.\"\"\"\n        worker = self.create_worker()\n        worker.poller = mock.Mock()\n        worker.tpool = mock.Mock()\n        worker.method_queue = mock.Mock()\n\n        assert worker.nr_conns == 0\n\n        # Simulate accept\n        client_sock = FakeSocket()\n        listener = mock.Mock()\n        listener.accept.return_value = (client_sock, ('127.0.0.1', 12345))\n        listener.getsockname.return_value = ('127.0.0.1', 8000)\n\n        worker.accept(listener)\n        assert worker.nr_conns == 1\n\n        # Simulate finish_request with close\n        conn = mock.Mock()\n        fs = mock.Mock()\n        fs.cancelled.return_value = False\n        fs.result.return_value = False  # Not keepalive\n        worker.finish_request(conn, fs)\n        assert worker.nr_conns == 0\n\n\nclass TestKeepaliveManagement:\n    \"\"\"Tests for keepalive connection management.\"\"\"\n\n    def create_worker(self):\n        \"\"\"Create a worker for testing.\"\"\"\n        cfg = Config()\n        cfg.set('workers', 1)\n        cfg.set('threads', 4)\n        cfg.set('worker_connections', 10)\n        cfg.set('keepalive', 2)\n\n        worker = gthread.ThreadWorker(\n            age=1,\n            ppid=os.getpid(),\n            sockets=[],\n            app=mock.Mock(),\n            timeout=30,\n            cfg=cfg,\n            log=mock.Mock(),\n        )\n        worker.poller = mock.Mock()\n        return worker\n\n    def test_max_keepalived_calculation(self):\n        \"\"\"Test that max_keepalived is correctly calculated.\"\"\"\n        worker = self.create_worker()\n        # max_keepalived = worker_connections - threads = 10 - 4 = 6\n        assert worker.max_keepalived == 6\n\n    def test_keepalive_timeout_ordering(self):\n        \"\"\"Test that connections are ordered by timeout for efficient murder.\"\"\"\n        worker = self.create_worker()\n\n        # Add connections with different timeouts\n        cfg = Config()\n        for i in range(3):\n            sock = FakeSocket()\n            conn = gthread.TConn(cfg, sock, ('127.0.0.1', 12345 + i), ('127.0.0.1', 8000))\n            conn.timeout = time.monotonic() + (i * 10)  # Staggered timeouts\n            worker.keepalived_conns.append(conn)\n            worker.nr_conns += 1\n\n        # First connection should have earliest timeout\n        first = worker.keepalived_conns[0]\n        last = worker.keepalived_conns[-1]\n        assert first.timeout < last.timeout\n\n    def test_murder_only_expired(self):\n        \"\"\"Test that only expired connections are closed.\"\"\"\n        worker = self.create_worker()\n        worker.poller = selectors.DefaultSelector()\n\n        cfg = Config()\n\n        # Add one expired and one valid connection\n        expired_sock = FakeSocket()\n        expired_conn = gthread.TConn(cfg, expired_sock, ('127.0.0.1', 12345), ('127.0.0.1', 8000))\n        expired_conn.timeout = time.monotonic() - 10  # Expired\n\n        valid_sock = FakeSocket()\n        valid_conn = gthread.TConn(cfg, valid_sock, ('127.0.0.1', 12346), ('127.0.0.1', 8000))\n        valid_conn.timeout = time.monotonic() + 100  # Still valid\n\n        worker.keepalived_conns.append(expired_conn)\n        worker.keepalived_conns.append(valid_conn)\n        worker.nr_conns = 2\n\n        with mock.patch.object(worker.poller, 'unregister'):\n            worker.murder_keepalived()\n\n        # Expired should be closed, valid should remain\n        assert expired_sock.closed is True\n        assert valid_sock.closed is False\n        assert len(worker.keepalived_conns) == 1\n        assert worker.keepalived_conns[0] is valid_conn\n        assert worker.nr_conns == 1\n\n        worker.poller.close()\n\n\nclass TestErrorHandling:\n    \"\"\"Tests for error handling in various scenarios.\"\"\"\n\n    def create_worker(self):\n        \"\"\"Create a worker for testing.\"\"\"\n        cfg = Config()\n        cfg.set('workers', 1)\n        cfg.set('threads', 4)\n        cfg.set('worker_connections', 1000)\n\n        worker = gthread.ThreadWorker(\n            age=1,\n            ppid=os.getpid(),\n            sockets=[],\n            app=mock.Mock(),\n            timeout=30,\n            cfg=cfg,\n            log=mock.Mock(),\n        )\n        worker.poller = mock.Mock()\n        return worker\n\n    def test_finish_request_handles_future_exception(self):\n        \"\"\"Test that finish_request handles exceptions from futures.\"\"\"\n        worker = self.create_worker()\n        worker.nr_conns = 1\n\n        conn = mock.Mock()\n        fs = mock.Mock()\n        fs.cancelled.return_value = False\n        fs.result.side_effect = RuntimeError(\"Worker crashed\")\n\n        # Should not raise, should close connection\n        worker.finish_request(conn, fs)\n\n        assert worker.nr_conns == 0\n        conn.close.assert_called_once()\n\n    def test_enqueue_req_submits_to_pool(self):\n        \"\"\"Test that enqueue_req properly submits to thread pool.\"\"\"\n        worker = self.create_worker()\n        worker.tpool = mock.Mock()\n        worker.method_queue = mock.Mock()\n\n        conn = mock.Mock()\n        worker.enqueue_req(conn)\n\n        worker.tpool.submit.assert_called_once()\n\n    def test_wait_for_events_handles_eintr(self):\n        \"\"\"Test that EINTR is handled gracefully.\"\"\"\n        worker = self.create_worker()\n        worker.poller = mock.Mock()\n        worker.poller.select.side_effect = OSError(errno.EINTR, \"Interrupted\")\n\n        # Should not raise\n        worker.wait_for_and_dispatch_events(1.0)\n\n    def test_wait_for_events_raises_other_errors(self):\n        \"\"\"Test that non-EINTR errors are propagated.\"\"\"\n        worker = self.create_worker()\n        worker.poller = mock.Mock()\n        worker.poller.select.side_effect = OSError(errno.EBADF, \"Bad file descriptor\")\n\n        with pytest.raises(OSError):\n            worker.wait_for_and_dispatch_events(1.0)\n\n\nclass TestConnectionState:\n    \"\"\"Tests for connection state management.\"\"\"\n\n    def test_tconn_double_init_is_safe(self):\n        \"\"\"Test that calling init() twice is safe (idempotent).\"\"\"\n        cfg = Config()\n        sock = FakeSocket()\n        conn = gthread.TConn(cfg, sock, ('127.0.0.1', 12345), ('127.0.0.1', 8000))\n\n        conn.init()\n        parser1 = conn.parser\n\n        conn.init()  # Should not reinitialize\n        parser2 = conn.parser\n\n        assert parser1 is parser2\n\n    def test_tconn_close_is_safe(self):\n        \"\"\"Test that closing a connection is safe.\"\"\"\n        cfg = Config()\n        sock = FakeSocket()\n        conn = gthread.TConn(cfg, sock, ('127.0.0.1', 12345), ('127.0.0.1', 8000))\n\n        conn.close()\n        assert sock.closed is True\n\n        # Second close should not raise\n        conn.close()\n\n    def test_keepalive_timeout_uses_monotonic(self):\n        \"\"\"Test that timeout uses monotonic clock.\"\"\"\n        cfg = Config()\n        cfg.set('keepalive', 5)\n        sock = FakeSocket()\n        conn = gthread.TConn(cfg, sock, ('127.0.0.1', 12345), ('127.0.0.1', 8000))\n\n        before = time.monotonic()\n        conn.set_timeout()\n        after = time.monotonic()\n\n        # Timeout should be approximately 5 seconds in the future\n        assert before + 4.9 <= conn.timeout <= after + 5.1\n\n\nclass TestWorkerLiveness:\n    \"\"\"Tests for worker liveness reporting to the arbiter.\"\"\"\n\n    def create_worker(self):\n        \"\"\"Create a worker for testing.\"\"\"\n        cfg = Config()\n        cfg.set('workers', 1)\n        cfg.set('threads', 4)\n        cfg.set('worker_connections', 1000)\n\n        worker = gthread.ThreadWorker(\n            age=1,\n            ppid=os.getpid(),\n            sockets=[],\n            app=mock.Mock(),\n            timeout=30,\n            cfg=cfg,\n            log=mock.Mock(),\n        )\n        return worker\n\n    def test_notify_calls_tmp_notify(self):\n        \"\"\"Test that worker.notify() calls tmp.notify() for arbiter monitoring.\"\"\"\n        worker = self.create_worker()\n        worker.tmp = mock.Mock()\n\n        worker.notify()\n\n        worker.tmp.notify.assert_called_once()\n\n    def test_notify_updates_tmp_mtime(self):\n        \"\"\"Test that notify updates the temp file mtime for arbiter heartbeat.\n\n        WorkerTmp.notify() sets mtime using time.monotonic(), and the arbiter\n        checks liveness by comparing (time.monotonic() - last_update()) to timeout.\n        \"\"\"\n        from gunicorn.workers.workertmp import WorkerTmp\n\n        cfg = Config()\n        tmp = WorkerTmp(cfg)\n\n        # Call notify to set mtime to current monotonic time\n        tmp.notify()\n\n        # The arbiter checks: time.monotonic() - last_update() <= timeout\n        # After notify(), this difference should be very small\n        diff = time.monotonic() - tmp.last_update()\n        assert diff < 1.0  # Should be nearly zero\n\n        # Wait and verify the difference grows\n        time.sleep(0.1)\n        diff_later = time.monotonic() - tmp.last_update()\n        assert diff_later > diff  # Time has passed\n\n        tmp.close()\n\n    def test_worker_notifies_in_run_loop(self):\n        \"\"\"Test that worker calls notify() during the run loop.\"\"\"\n        worker = self.create_worker()\n        worker.tmp = mock.Mock()\n        worker.method_queue.init()\n        worker.poller = mock.Mock()\n        worker.tpool = mock.Mock()\n        worker.sockets = []\n        worker.alive = True\n\n        # Track notify calls\n        notify_calls = []\n        original_notify = worker.notify\n\n        def tracking_notify():\n            notify_calls.append(time.monotonic())\n            original_notify()\n\n        worker.notify = tracking_notify\n\n        # Mock poller.select to exit after first iteration\n        call_count = [0]\n\n        def mock_select(timeout):\n            call_count[0] += 1\n            if call_count[0] > 1:\n                worker.alive = False\n            return []\n\n        worker.poller.select.side_effect = mock_select\n\n        # Mock is_parent_alive to return True\n        worker.is_parent_alive = mock.Mock(return_value=True)\n\n        worker.run()\n\n        # Worker should have called notify at least once\n        assert len(notify_calls) >= 1\n        worker.method_queue.close()\n\n\nclass TestSignalHandling:\n    \"\"\"Tests for signal handling in gthread worker.\"\"\"\n\n    def create_worker(self):\n        \"\"\"Create a worker for testing.\"\"\"\n        cfg = Config()\n        cfg.set('workers', 1)\n        cfg.set('threads', 4)\n        cfg.set('worker_connections', 1000)\n        cfg.set('graceful_timeout', 5)\n\n        worker = gthread.ThreadWorker(\n            age=1,\n            ppid=os.getpid(),\n            sockets=[],\n            app=mock.Mock(),\n            timeout=30,\n            cfg=cfg,\n            log=mock.Mock(),\n        )\n        return worker\n\n    def test_handle_exit_sigterm_sets_alive_false(self):\n        \"\"\"Test that SIGTERM handler sets alive=False for graceful shutdown.\"\"\"\n        worker = self.create_worker()\n        worker.method_queue.init()\n        worker.alive = True\n\n        # Simulate SIGTERM\n        worker.handle_exit(None, None)\n\n        assert worker.alive is False\n        worker.method_queue.close()\n\n    def test_handle_exit_wakes_up_poller(self):\n        \"\"\"Test that SIGTERM handler wakes up the poller via method_queue.\"\"\"\n        worker = self.create_worker()\n        worker.method_queue.init()\n        worker.alive = True\n\n        # After handle_exit, the method_queue should have a callback queued\n        worker.handle_exit(None, None)\n\n        # Check that something was written to the pipe (to wake poller)\n        # Read from the pipe - should have data\n        import select\n        readable, _, _ = select.select([worker.method_queue.fileno()], [], [], 0)\n        assert len(readable) > 0\n\n        worker.method_queue.close()\n\n    def test_handle_quit_sigquit_immediate_shutdown(self):\n        \"\"\"Test that SIGQUIT handler triggers immediate shutdown.\"\"\"\n        worker = self.create_worker()\n        worker.tpool = mock.Mock()\n\n        with pytest.raises(SystemExit) as exc_info:\n            worker.handle_quit(None, None)\n\n        assert exc_info.value.code == 0\n        worker.tpool.shutdown.assert_called_once_with(wait=False)\n\n    def test_graceful_shutdown_stops_accepting(self):\n        \"\"\"Test that graceful shutdown stops accepting new connections.\"\"\"\n        worker = self.create_worker()\n        worker.method_queue.init()\n        worker.poller = mock.Mock()\n        worker.tpool = mock.Mock()\n        worker.sockets = [mock.Mock()]\n        worker._accepting = True\n\n        # Start accepting\n        worker.set_accept_enabled(True)\n\n        # Simulate SIGTERM\n        worker.handle_exit(None, None)\n        assert worker.alive is False\n\n        # During run loop, accepting should be disabled\n        worker.set_accept_enabled(False)\n        assert worker._accepting is False\n\n        worker.method_queue.close()\n\n    def test_graceful_shutdown_drains_connections(self):\n        \"\"\"Test that graceful shutdown waits for connections to drain.\"\"\"\n        worker = self.create_worker()\n        worker.method_queue.init()\n        worker.poller = mock.Mock()\n        worker.poller.select.return_value = []\n        worker.tpool = mock.Mock()\n        worker.sockets = []\n        worker.nr_conns = 1  # One active connection\n        worker.alive = True\n\n        # Track iterations\n        iterations = [0]\n\n        def mock_select(timeout):\n            iterations[0] += 1\n            if iterations[0] == 1:\n                # First iteration: trigger shutdown\n                worker.alive = False\n            elif iterations[0] == 2:\n                # Second iteration: during grace period\n                pass\n            elif iterations[0] >= 3:\n                # Connection finishes\n                worker.nr_conns = 0\n            return []\n\n        worker.poller.select.side_effect = mock_select\n        worker.is_parent_alive = mock.Mock(return_value=True)\n\n        worker.run()\n\n        # Should have waited for connections\n        assert iterations[0] >= 2\n        worker.method_queue.close()\n\n    def test_sigterm_does_not_interrupt_active_request(self):\n        \"\"\"Test that SIGTERM doesn't immediately interrupt active requests.\"\"\"\n        import signal\n\n        worker = self.create_worker()\n        worker.method_queue.init()\n\n        # The base worker sets siginterrupt(SIGTERM, False) in init_signals\n        # This ensures system calls aren't interrupted by SIGTERM\n\n        # Verify handle_exit just sets alive=False, doesn't raise\n        worker.alive = True\n        worker.handle_exit(signal.SIGTERM, None)\n\n        assert worker.alive is False\n        # No exception raised, request can continue\n        worker.method_queue.close()\n\n\nclass TestWorkerArbiterIntegration:\n    \"\"\"Integration tests for worker-arbiter communication.\"\"\"\n\n    def create_worker(self):\n        \"\"\"Create a worker for testing.\"\"\"\n        cfg = Config()\n        cfg.set('workers', 1)\n        cfg.set('threads', 4)\n        cfg.set('worker_connections', 1000)\n        cfg.set('graceful_timeout', 2)\n\n        worker = gthread.ThreadWorker(\n            age=1,\n            ppid=os.getpid(),\n            sockets=[],\n            app=mock.Mock(),\n            timeout=30,\n            cfg=cfg,\n            log=mock.Mock(),\n        )\n        return worker\n\n    def test_worker_detects_parent_death(self):\n        \"\"\"Test that worker detects when parent process dies.\"\"\"\n        worker = self.create_worker()\n\n        # Valid ppid\n        worker.ppid = os.getppid()\n        assert worker.is_parent_alive() is True\n\n        # Invalid ppid (simulating parent death)\n        worker.ppid = 99999999\n        assert worker.is_parent_alive() is False\n\n    def test_worker_exits_on_parent_death(self):\n        \"\"\"Test that worker exits when parent dies.\"\"\"\n        worker = self.create_worker()\n        worker.method_queue.init()\n        worker.poller = mock.Mock()\n        worker.poller.select.return_value = []\n        worker.tpool = mock.Mock()\n        worker.sockets = []\n        worker.alive = True\n        worker.ppid = 99999999  # Invalid ppid\n\n        iterations = [0]\n\n        def mock_select(timeout):\n            iterations[0] += 1\n            return []\n\n        worker.poller.select.side_effect = mock_select\n\n        worker.run()\n\n        # Should exit immediately due to parent check\n        assert iterations[0] == 1\n        worker.method_queue.close()\n\n    def test_worker_tmp_file_can_be_monitored(self):\n        \"\"\"Test that worker tmp file can be used by arbiter for monitoring.\n\n        The arbiter monitors workers by checking: time.monotonic() - last_update() <= timeout\n        \"\"\"\n        from gunicorn.workers.workertmp import WorkerTmp\n\n        cfg = Config()\n        tmp = WorkerTmp(cfg)\n\n        # Worker notifies - sets mtime to current monotonic time\n        tmp.notify()\n\n        # Arbiter check: time.monotonic() - last_update() should be small\n        diff = time.monotonic() - tmp.last_update()\n        assert diff < 1.0  # Worker just notified, should be nearly zero\n\n        # If worker stops notifying, the difference grows\n        time.sleep(0.1)\n        diff_later = time.monotonic() - tmp.last_update()\n        assert diff_later > diff  # Arbiter would notice worker isn't responding\n\n        tmp.close()\n\n    def test_graceful_timeout_honored(self):\n        \"\"\"Test that graceful_timeout is honored during shutdown.\"\"\"\n        worker = self.create_worker()\n        worker.cfg.set('graceful_timeout', 1)  # 1 second for testing\n        worker.method_queue.init()\n        worker.poller = mock.Mock()\n        worker.tpool = mock.Mock()\n        worker.sockets = []\n        worker.nr_conns = 1  # Active connection that won't finish\n        worker.alive = True\n\n        # Track iterations\n        iterations = [0]\n        start_time = [None]\n\n        def mock_select(timeout):\n            iterations[0] += 1\n            if iterations[0] == 1:\n                # First iteration: trigger shutdown\n                worker.alive = False\n                start_time[0] = time.monotonic()\n                return []\n            else:\n                # Grace period iterations - simulate time passing via select timeout\n                # The timeout should be the remaining time\n                if timeout > 0:\n                    # Simulate some time passing\n                    time.sleep(min(timeout, 0.2))\n                # Connection never finishes (nr_conns stays 1)\n                return []\n        worker.poller.select.side_effect = mock_select\n        worker.is_parent_alive = mock.Mock(return_value=True)\n\n        worker.run()\n\n        # Should have completed (grace timeout expired with connection still active)\n        assert iterations[0] >= 2  # At least one grace period iteration\n\n        worker.method_queue.close()\n\n    def test_run_completes_cleanup(self):\n        \"\"\"Test that run() properly cleans up resources on exit.\"\"\"\n        worker = self.create_worker()\n        worker.method_queue.init()\n        worker.poller = selectors.DefaultSelector()\n        worker.tpool = futures.ThreadPoolExecutor(max_workers=2)\n        worker.sockets = []\n        worker.alive = False  # Immediately exit\n\n        worker.is_parent_alive = mock.Mock(return_value=True)\n\n        # Don't pre-register method_queue - run() will do it\n        worker.run()\n\n        # All resources should be cleaned up\n        # (No assertion needed - if run() completes without error, cleanup worked)\n\n\nclass TestSignalInteraction:\n    \"\"\"Tests for signal interactions and edge cases.\"\"\"\n\n    def create_worker(self):\n        \"\"\"Create a worker for testing.\"\"\"\n        cfg = Config()\n        cfg.set('workers', 1)\n        cfg.set('threads', 4)\n        cfg.set('worker_connections', 1000)\n\n        worker = gthread.ThreadWorker(\n            age=1,\n            ppid=os.getpid(),\n            sockets=[],\n            app=mock.Mock(),\n            timeout=30,\n            cfg=cfg,\n            log=mock.Mock(),\n        )\n        return worker\n\n    def test_multiple_sigterm_is_safe(self):\n        \"\"\"Test that receiving multiple SIGTERM is safe.\"\"\"\n        worker = self.create_worker()\n        worker.method_queue.init()\n        worker.alive = True\n\n        # Multiple SIGTERM calls should be idempotent\n        worker.handle_exit(None, None)\n        assert worker.alive is False\n\n        worker.handle_exit(None, None)\n        assert worker.alive is False\n\n        worker.method_queue.close()\n\n    def test_sigterm_then_sigquit(self):\n        \"\"\"Test SIGQUIT after SIGTERM for force kill.\"\"\"\n        worker = self.create_worker()\n        worker.method_queue.init()\n        worker.tpool = mock.Mock()\n        worker.alive = True\n\n        # First SIGTERM for graceful\n        worker.handle_exit(None, None)\n        assert worker.alive is False\n\n        # Then SIGQUIT for immediate\n        with pytest.raises(SystemExit):\n            worker.handle_quit(None, None)\n\n        worker.tpool.shutdown.assert_called_once_with(wait=False)\n        worker.method_queue.close()\n\n    def test_sigquit_does_not_wait_for_threads(self):\n        \"\"\"Test that SIGQUIT calls tpool.shutdown(wait=False).\"\"\"\n        worker = self.create_worker()\n        worker.tpool = mock.Mock()\n\n        with pytest.raises(SystemExit):\n            worker.handle_quit(None, None)\n\n        # Verify wait=False was passed\n        worker.tpool.shutdown.assert_called_once_with(wait=False)\n\n    def test_handle_exit_when_already_dead(self):\n        \"\"\"Test handle_exit when worker is already shutting down.\"\"\"\n        worker = self.create_worker()\n        worker.method_queue.init()\n        worker.alive = False\n\n        # Should not raise, should be idempotent\n        worker.handle_exit(None, None)\n        assert worker.alive is False\n\n        worker.method_queue.close()\n\n    def test_connections_tracked_during_signal(self):\n        \"\"\"Test that connection count is correct during signal handling.\"\"\"\n        worker = self.create_worker()\n        worker.method_queue.init()\n        worker.poller = mock.Mock()\n        worker.tpool = mock.Mock()\n        worker.nr_conns = 5\n        worker.alive = True\n\n        # SIGTERM should not affect connection count\n        worker.handle_exit(None, None)\n\n        assert worker.nr_conns == 5  # Still 5 connections\n        assert worker.alive is False  # But shutting down\n\n        worker.method_queue.close()\n\n\nclass TestKeepaliveBlockingMode:\n    \"\"\"Tests for socket blocking mode on keepalive connections (issue #3448).\"\"\"\n\n    def create_worker(self):\n        \"\"\"Create a worker for testing.\"\"\"\n        cfg = Config()\n        cfg.set('workers', 1)\n        cfg.set('threads', 4)\n        cfg.set('worker_connections', 1000)\n        cfg.set('keepalive', 2)\n\n        worker = gthread.ThreadWorker(\n            age=1,\n            ppid=os.getpid(),\n            sockets=[],\n            app=mock.Mock(),\n            timeout=30,\n            cfg=cfg,\n            log=mock.Mock(),\n        )\n        return worker\n\n    def test_handle_sets_blocking_on_keepalive_connection(self):\n        \"\"\"Test that handle() sets socket to blocking mode on keepalive connections.\n\n        On keepalive connections, the socket is in non-blocking mode (set by\n        finish_request() for the selector). handle() must set it back to blocking\n        before reading request/body to avoid SSLWantReadError on SSL connections.\n        \"\"\"\n        worker = self.create_worker()\n        worker.wsgi = mock.Mock(return_value=[b'response'])\n\n        # Create a connection that simulates a keepalive reuse\n        cfg = Config()\n        sock = FakeSocket()\n        conn = gthread.TConn(cfg, sock, ('127.0.0.1', 12345), ('127.0.0.1', 8000))\n\n        # Simulate the state after finish_request() for keepalive:\n        # - socket is non-blocking (for selector registration)\n        # - connection is already initialized\n        conn.init()  # First request initialized the connection\n        sock.setblocking(False)  # finish_request() set non-blocking for selector\n        assert sock.blocking is False\n        assert conn.initialized is True\n\n        # Verify that handle() sets the socket to blocking mode\n        # Mock the parser to avoid actually parsing\n        mock_parser = mock.Mock()\n        mock_parser.__next__ = mock.Mock(return_value=None)  # No request\n        conn.parser = mock_parser\n\n        worker.handle(conn)\n\n        # Socket should be set to blocking mode by handle()\n        assert sock.blocking is True\n\n    def test_handle_sets_blocking_before_body_read(self):\n        \"\"\"Test that socket is blocking before WSGI app reads request body.\n\n        This is the core fix for issue #3448: Flask's request.get_json()\n        reads the body, which triggers socket.recv(). If the socket is\n        non-blocking, this raises SSLWantReadError on SSL connections.\n        \"\"\"\n        worker = self.create_worker()\n\n        cfg = Config()\n        sock = FakeSocket()\n        conn = gthread.TConn(cfg, sock, ('127.0.0.1', 12345), ('127.0.0.1', 8000))\n\n        # Simulate keepalive state\n        conn.init()\n        sock.setblocking(False)\n\n        # Track when blocking is set vs when body would be read\n        blocking_state_at_body_read = [None]\n\n        def mock_wsgi(environ, start_response):\n            # This simulates Flask's request.get_json() reading the body\n            # The socket must be blocking at this point\n            blocking_state_at_body_read[0] = sock.blocking\n            start_response('200 OK', [])\n            return [b'response']\n\n        worker.wsgi = mock_wsgi\n\n        # Mock parser to return a request\n        mock_request = mock.Mock()\n        mock_request.headers = []\n        mock_request.unreader = mock.Mock()\n        mock_request.body = mock.Mock()\n        mock_request.body.read.return_value = b''\n\n        mock_parser = mock.Mock()\n        mock_parser.__next__ = mock.Mock(return_value=mock_request)\n        mock_parser.finish_body = mock.Mock()\n        conn.parser = mock_parser\n\n        # Mock handle_request to invoke wsgi\n        _ = worker.handle_request  # save reference before overwriting\n\n        def mock_handle_request(req, conn):\n            # Simplified version that just calls wsgi\n            worker.wsgi({}, lambda s, h: None)\n            return True\n\n        worker.handle_request = mock_handle_request\n\n        worker.handle(conn)\n\n        # Socket must be blocking when WSGI app reads body\n        assert blocking_state_at_body_read[0] is True\n\n\nclass TestFinishBodySSL:\n    \"\"\"Tests for SSL error handling in finish_body().\"\"\"\n\n    def test_finish_body_handles_ssl_want_read_error(self):\n        \"\"\"Test that finish_body() handles SSLWantReadError gracefully.\n\n        When discarding unread body data on SSL connections, the socket\n        may raise SSLWantReadError if there's no application data available.\n        This should be treated as \"no more data\" rather than an error.\n        \"\"\"\n        import ssl\n        from gunicorn.http.parser import RequestParser\n\n        # Create a mock SSL socket that raises SSLWantReadError on recv\n        class MockSSLSocket:\n            def __init__(self):\n                self._fileno = 123\n\n            def fileno(self):\n                return self._fileno\n\n            def recv(self, size):\n                raise ssl.SSLWantReadError(\"The operation did not complete\")\n\n            def setblocking(self, blocking):\n                pass\n\n        cfg = Config()\n        sock = MockSSLSocket()\n        parser = RequestParser(cfg, sock, ('127.0.0.1', 12345))\n\n        # Create a mock message with a body that will trigger socket read\n        mock_body = mock.Mock()\n        mock_body.read.side_effect = ssl.SSLWantReadError(\"The operation did not complete\")\n\n        mock_mesg = mock.Mock()\n        mock_mesg.body = mock_body\n        parser.mesg = mock_mesg\n\n        # finish_body() should handle SSLWantReadError without raising\n        parser.finish_body()  # Should not raise\n\n        # Verify body.read was called\n        mock_body.read.assert_called_once_with(1024)\n\n    def test_finish_body_reads_all_data_before_ssl_error(self):\n        \"\"\"Test that finish_body() reads all available data before SSLWantReadError.\"\"\"\n        import ssl\n        from gunicorn.http.parser import RequestParser\n\n        cfg = Config()\n\n        # Create a mock socket\n        class MockSocket:\n            def recv(self, size):\n                return b''\n\n            def setblocking(self, blocking):\n                pass\n\n        sock = MockSocket()\n        parser = RequestParser(cfg, sock, ('127.0.0.1', 12345))\n\n        # Create a mock message body that returns data then raises SSLWantReadError\n        call_count = [0]\n\n        def mock_read(size):\n            call_count[0] += 1\n            if call_count[0] <= 2:\n                return b'x' * size  # Return data first two times\n            raise ssl.SSLWantReadError(\"The operation did not complete\")\n\n        mock_body = mock.Mock()\n        mock_body.read.side_effect = mock_read\n\n        mock_mesg = mock.Mock()\n        mock_mesg.body = mock_body\n        parser.mesg = mock_mesg\n\n        # finish_body() should read all data and handle SSLWantReadError\n        parser.finish_body()  # Should not raise\n\n        # Verify body.read was called multiple times (2 data reads + 1 error)\n        assert call_count[0] == 3\n\n    def test_finish_body_normal_operation(self):\n        \"\"\"Test that finish_body() works normally when no SSL error occurs.\"\"\"\n        from gunicorn.http.parser import RequestParser\n\n        cfg = Config()\n\n        class MockSocket:\n            def recv(self, size):\n                return b''\n\n            def setblocking(self, blocking):\n                pass\n\n        sock = MockSocket()\n        parser = RequestParser(cfg, sock, ('127.0.0.1', 12345))\n\n        # Create a mock message body that returns empty (end of data)\n        mock_body = mock.Mock()\n        mock_body.read.return_value = b''\n\n        mock_mesg = mock.Mock()\n        mock_mesg.body = mock_body\n        parser.mesg = mock_mesg\n\n        # finish_body() should work normally\n        parser.finish_body()\n\n        # Verify body.read was called once and returned empty\n        mock_body.read.assert_called_once_with(1024)\n\n\nclass TestHTTP2TrailerCallback:\n    \"\"\"Tests for HTTP/2 response trailer callback.\"\"\"\n\n    def test_trailer_callback_stores_trailers(self):\n        \"\"\"Test that the trailer callback stores trailers for later sending.\"\"\"\n        # Simulate the trailer callback pattern used in handle_http2_request\n        pending_trailers = []\n\n        def send_trailers_h2(trailers):\n            \"\"\"Queue trailers to be sent after response body.\"\"\"\n            pending_trailers.extend(trailers)\n\n        # Call the callback with trailers\n        send_trailers_h2([('grpc-status', '0'), ('grpc-message', 'OK')])\n\n        assert len(pending_trailers) == 2\n        assert pending_trailers[0] == ('grpc-status', '0')\n        assert pending_trailers[1] == ('grpc-message', 'OK')\n\n    def test_trailer_callback_multiple_calls(self):\n        \"\"\"Test that multiple calls to trailer callback accumulate trailers.\"\"\"\n        pending_trailers = []\n\n        def send_trailers_h2(trailers):\n            pending_trailers.extend(trailers)\n\n        # Call multiple times\n        send_trailers_h2([('grpc-status', '0')])\n        send_trailers_h2([('grpc-message', 'OK')])\n        send_trailers_h2([('server-timing', 'total;dur=100')])\n\n        assert len(pending_trailers) == 3\n        assert pending_trailers == [\n            ('grpc-status', '0'),\n            ('grpc-message', 'OK'),\n            ('server-timing', 'total;dur=100'),\n        ]\n\n    def test_trailer_callback_empty_list(self):\n        \"\"\"Test that empty trailer list is handled correctly.\"\"\"\n        pending_trailers = []\n\n        def send_trailers_h2(trailers):\n            pending_trailers.extend(trailers)\n\n        send_trailers_h2([])\n\n        assert len(pending_trailers) == 0\n\n\nclass TestSlowClientResilience:\n    \"\"\"Tests for slow client handling to prevent thread pool exhaustion.\"\"\"\n\n    def create_worker(self, cfg=None):\n        \"\"\"Helper to create a ThreadWorker for testing.\"\"\"\n        if cfg is None:\n            cfg = Config()\n        cfg.set('threads', 4)\n        cfg.set('worker_connections', 1000)\n        cfg.set('keepalive', 5)\n\n        worker = gthread.ThreadWorker(\n            age=1,\n            ppid=os.getpid(),\n            sockets=[],\n            app=mock.Mock(),\n            timeout=30,\n            cfg=cfg,\n            log=mock.Mock(),\n        )\n        return worker\n\n    def test_tconn_wait_for_data_returns_true_when_ready(self):\n        \"\"\"Test wait_for_data returns True when data_ready is already set.\"\"\"\n        cfg = Config()\n        sock = FakeSocket()\n        conn = gthread.TConn(cfg, sock, ('127.0.0.1', 12345), ('127.0.0.1', 8000))\n        conn.data_ready = True\n\n        # Should return True immediately without waiting\n        assert conn.wait_for_data(5.0) is True\n\n    def test_tconn_wait_for_data_sets_data_ready(self):\n        \"\"\"Test wait_for_data sets data_ready flag when data arrives.\"\"\"\n        import socket as stdlib_socket\n        # Create a real socket pair to test selector behavior\n        server, client = stdlib_socket.socketpair()\n        try:\n            cfg = Config()\n            conn = gthread.TConn(cfg, server, ('127.0.0.1', 12345), ('127.0.0.1', 8000))\n            conn.data_ready = False\n\n            # Send data from client\n            client.send(b'GET / HTTP/1.1\\r\\n')\n\n            # Should detect data is ready\n            result = conn.wait_for_data(1.0)\n\n            assert result is True\n            assert conn.data_ready is True\n        finally:\n            server.close()\n            client.close()\n\n    def test_tconn_wait_for_data_timeout(self):\n        \"\"\"Test wait_for_data returns False on timeout.\"\"\"\n        import socket as stdlib_socket\n        # Create a real socket pair but don't send any data\n        server, client = stdlib_socket.socketpair()\n        try:\n            cfg = Config()\n            conn = gthread.TConn(cfg, server, ('127.0.0.1', 12345), ('127.0.0.1', 8000))\n            conn.data_ready = False\n\n            # Don't send any data - should timeout\n            start = time.monotonic()\n            result = conn.wait_for_data(0.1)  # Short timeout\n            elapsed = time.monotonic() - start\n\n            assert result is False\n            assert conn.data_ready is False\n            assert elapsed >= 0.1\n        finally:\n            server.close()\n            client.close()\n\n    def test_finish_request_handles_defer(self):\n        \"\"\"Test finish_request puts deferred connections back on poller.\"\"\"\n        worker = self.create_worker()\n        worker.poller = mock.Mock()\n        worker.pending_conns = deque()\n        worker.nr_conns = 1\n        worker.alive = True\n\n        sock = FakeSocket()\n        conn = gthread.TConn(worker.cfg, sock, ('127.0.0.1', 12345), ('127.0.0.1', 8000))\n\n        # Create a future that returns _DEFER\n        fs = mock.Mock()\n        fs.cancelled.return_value = False\n        fs.result.return_value = gthread._DEFER\n\n        worker.finish_request(conn, fs)\n\n        # Connection should be in pending_conns, not closed\n        assert len(worker.pending_conns) == 1\n        assert worker.pending_conns[0] is conn\n        assert worker.nr_conns == 1  # Still counted\n        assert not sock.closed\n\n        # Should be registered with poller\n        worker.poller.register.assert_called_once()\n\n    def test_on_pending_socket_readable_sets_data_ready(self):\n        \"\"\"Test on_pending_socket_readable marks connection data as ready.\"\"\"\n        worker = self.create_worker()\n        worker.poller = mock.Mock()\n        worker.tpool = mock.Mock()\n        worker.method_queue = mock.Mock()\n        worker.pending_conns = deque()\n\n        sock = FakeSocket()\n        conn = gthread.TConn(worker.cfg, sock, ('127.0.0.1', 12345), ('127.0.0.1', 8000))\n        conn.data_ready = False\n        worker.pending_conns.append(conn)\n\n        # Simulate socket becoming readable\n        worker.on_pending_socket_readable(conn, sock)\n\n        assert conn.data_ready is True\n        assert conn not in worker.pending_conns\n        worker.poller.unregister.assert_called_once_with(sock)\n        worker.tpool.submit.assert_called_once()\n\n    def test_murder_pending_closes_expired_connections(self):\n        \"\"\"Test murder_pending closes connections that have timed out.\"\"\"\n        worker = self.create_worker()\n        worker.poller = mock.Mock()\n        worker.pending_conns = deque()\n        worker.nr_conns = 2\n\n        # Create two connections, one expired, one not\n        sock1 = FakeSocket()\n        conn1 = gthread.TConn(worker.cfg, sock1, ('127.0.0.1', 12345), ('127.0.0.1', 8000))\n        conn1.timeout = time.monotonic() - 10  # Expired\n\n        sock2 = FakeSocket()\n        conn2 = gthread.TConn(worker.cfg, sock2, ('127.0.0.1', 12346), ('127.0.0.1', 8000))\n        conn2.timeout = time.monotonic() + 100  # Not expired\n\n        worker.pending_conns.append(conn1)\n        worker.pending_conns.append(conn2)\n\n        worker.murder_pending()\n\n        # Only expired connection should be closed\n        assert sock1.closed\n        assert not sock2.closed\n        assert len(worker.pending_conns) == 1\n        assert worker.pending_conns[0] is conn2\n        assert worker.nr_conns == 1\n\n    def test_handle_defers_slow_connection(self):\n        \"\"\"Test that handle() returns _DEFER for connections without data.\"\"\"\n        worker = self.create_worker()\n\n        # Create a connection that will timeout waiting for data\n        sock = mock.Mock()\n        conn = gthread.TConn(worker.cfg, sock, ('127.0.0.1', 12345), ('127.0.0.1', 8000))\n        conn.initialized = False\n        conn.data_ready = False\n\n        # Mock wait_for_data to simulate timeout\n        conn.wait_for_data = mock.Mock(return_value=False)\n\n        result = worker.handle(conn)\n\n        assert result is gthread._DEFER\n        conn.wait_for_data.assert_called_once()\n\n    def test_handle_processes_fast_connection(self):\n        \"\"\"Test that handle() processes connections with data immediately.\"\"\"\n        worker = self.create_worker()\n        worker.wsgi = mock.Mock(return_value=[b'OK'])\n\n        # Create a connection with data ready\n        sock = mock.Mock()\n        conn = gthread.TConn(worker.cfg, sock, ('127.0.0.1', 12345), ('127.0.0.1', 8000))\n        conn.initialized = False\n        conn.data_ready = True  # Data is ready\n\n        # Mock init and parser\n        conn.init = mock.Mock()\n        conn.parser = mock.Mock()\n        conn.parser.__next__ = mock.Mock(return_value=None)  # No request parsed\n\n        result = worker.handle(conn)\n\n        # Should not return _DEFER since data was ready\n        assert result is not gthread._DEFER\n        conn.init.assert_called_once()\n\n    def test_handle_skips_wait_for_initialized_connections(self):\n        \"\"\"Test handle() skips wait_for_data for already initialized (keepalive) connections.\"\"\"\n        worker = self.create_worker()\n        worker.wsgi = mock.Mock(return_value=[b'OK'])\n\n        sock = mock.Mock()\n        conn = gthread.TConn(worker.cfg, sock, ('127.0.0.1', 12345), ('127.0.0.1', 8000))\n        conn.initialized = True  # Already initialized (keepalive)\n        conn.data_ready = False\n        conn.wait_for_data = mock.Mock()\n\n        conn.parser = mock.Mock()\n        conn.parser.__next__ = mock.Mock(return_value=None)\n\n        worker.handle(conn)\n\n        # wait_for_data should not be called for initialized connections\n        conn.wait_for_data.assert_not_called()\n"
  },
  {
    "path": "tests/test_gtornado.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Tests for the tornado worker.\"\"\"\n\nimport os\nfrom unittest import mock\n\nimport pytest\n\ntornado = pytest.importorskip(\"tornado\")\n\nfrom gunicorn.config import Config\nfrom gunicorn.workers import gtornado\n\n\nclass FakeSocket:\n    \"\"\"Mock socket for testing.\"\"\"\n\n    def __init__(self, data=b''):\n        self.data = data\n        self.closed = False\n        self.blocking = True\n        self._fileno = id(self) % 65536\n\n    def fileno(self):\n        return self._fileno\n\n    def setblocking(self, blocking):\n        self.blocking = blocking\n\n    def recv(self, size):\n        result = self.data[:size]\n        self.data = self.data[size:]\n        return result\n\n    def send(self, data):\n        return len(data)\n\n    def close(self):\n        self.closed = True\n\n    def getsockname(self):\n        return ('127.0.0.1', 8000)\n\n    def getpeername(self):\n        return ('127.0.0.1', 12345)\n\n\nclass TestTornadoWorkerInit:\n    \"\"\"Tests for TornadoWorker initialization.\"\"\"\n\n    def create_worker(self, cfg=None):\n        \"\"\"Create a worker instance for testing.\"\"\"\n        if cfg is None:\n            cfg = Config()\n        cfg.set('workers', 1)\n        cfg.set('max_requests', 0)\n\n        worker = gtornado.TornadoWorker(\n            age=1,\n            ppid=os.getpid(),\n            sockets=[],\n            app=mock.Mock(),\n            timeout=30,\n            cfg=cfg,\n            log=mock.Mock(),\n        )\n        return worker\n\n    def test_worker_init(self):\n        \"\"\"Test worker initialization.\"\"\"\n        worker = self.create_worker()\n        assert worker.nr == 0\n\n    def test_init_process_clears_ioloop(self):\n        \"\"\"Test that init_process clears the current IOLoop.\"\"\"\n        worker = self.create_worker()\n        worker.tmp = mock.Mock()\n        worker.log = mock.Mock()\n\n        with mock.patch.object(gtornado.IOLoop, 'clear_current') as mock_clear:\n            with mock.patch.object(gtornado.Worker, 'init_process'):\n                worker.init_process()\n            mock_clear.assert_called_once()\n\n\nclass TestRequestCounting:\n    \"\"\"Tests for request counting and max_requests behavior.\"\"\"\n\n    def create_worker(self, cfg=None):\n        \"\"\"Create a worker instance for testing.\"\"\"\n        if cfg is None:\n            cfg = Config()\n        cfg.set('workers', 1)\n\n        worker = gtornado.TornadoWorker(\n            age=1,\n            ppid=os.getpid(),\n            sockets=[],\n            app=mock.Mock(),\n            timeout=30,\n            cfg=cfg,\n            log=mock.Mock(),\n        )\n        return worker\n\n    def test_handle_request_increments_counter(self):\n        \"\"\"Test that handle_request increments the request counter.\"\"\"\n        worker = self.create_worker()\n        worker.nr = 0\n        worker.max_requests = 100\n        worker.alive = True\n\n        worker.handle_request()\n\n        assert worker.nr == 1\n        assert worker.alive is True\n\n    def test_max_requests_triggers_shutdown(self):\n        \"\"\"Test that reaching max_requests triggers shutdown.\"\"\"\n        cfg = Config()\n        cfg.set('max_requests', 5)\n        worker = self.create_worker(cfg)\n        worker.nr = 4\n        worker.alive = True\n        worker.max_requests = 5\n\n        worker.handle_request()\n\n        assert worker.nr == 5\n        assert worker.alive is False\n\n\nclass TestSignalHandling:\n    \"\"\"Tests for signal handling in tornado worker.\"\"\"\n\n    def create_worker(self):\n        \"\"\"Create a worker for testing.\"\"\"\n        cfg = Config()\n        cfg.set('workers', 1)\n\n        worker = gtornado.TornadoWorker(\n            age=1,\n            ppid=os.getpid(),\n            sockets=[],\n            app=mock.Mock(),\n            timeout=30,\n            cfg=cfg,\n            log=mock.Mock(),\n        )\n        return worker\n\n    def test_handle_exit_sets_alive_false(self):\n        \"\"\"Test that handle_exit sets alive=False through parent.\"\"\"\n        worker = self.create_worker()\n        worker.alive = True\n\n        # The parent's handle_exit is what sets alive=False\n        worker.handle_exit(None, None)\n\n        assert worker.alive is False\n\n    def test_handle_exit_only_once(self):\n        \"\"\"Test that handle_exit only triggers once when alive.\"\"\"\n        worker = self.create_worker()\n        worker.alive = True\n\n        # First call should set alive=False\n        worker.handle_exit(None, None)\n        assert worker.alive is False\n\n        # Second call should do nothing (alive is already False)\n        # Track that super().handle_exit is not called again\n        with mock.patch.object(gtornado.Worker, 'handle_exit') as mock_exit:\n            worker.handle_exit(None, None)\n            mock_exit.assert_not_called()\n\n\nclass TestWatchdog:\n    \"\"\"Tests for watchdog functionality.\"\"\"\n\n    def create_worker(self):\n        \"\"\"Create a worker for testing.\"\"\"\n        cfg = Config()\n        cfg.set('workers', 1)\n\n        worker = gtornado.TornadoWorker(\n            age=1,\n            ppid=os.getpid(),\n            sockets=[],\n            app=mock.Mock(),\n            timeout=30,\n            cfg=cfg,\n            log=mock.Mock(),\n        )\n        return worker\n\n    def test_watchdog_notifies_when_alive(self):\n        \"\"\"Test that watchdog calls notify when alive.\"\"\"\n        worker = self.create_worker()\n        worker.alive = True\n        worker.ppid = os.getppid()\n        worker.tmp = mock.Mock()\n\n        worker.watchdog()\n\n        worker.tmp.notify.assert_called_once()\n\n    def test_watchdog_detects_parent_death(self):\n        \"\"\"Test that watchdog detects parent death.\"\"\"\n        worker = self.create_worker()\n        worker.alive = True\n        worker.ppid = 99999999  # Invalid ppid\n        worker.tmp = mock.Mock()\n\n        worker.watchdog()\n\n        assert worker.alive is False\n\n\nclass TestHeartbeat:\n    \"\"\"Tests for heartbeat functionality.\"\"\"\n\n    def create_worker(self):\n        \"\"\"Create a worker for testing.\"\"\"\n        cfg = Config()\n        cfg.set('workers', 1)\n\n        worker = gtornado.TornadoWorker(\n            age=1,\n            ppid=os.getpid(),\n            sockets=[],\n            app=mock.Mock(),\n            timeout=30,\n            cfg=cfg,\n            log=mock.Mock(),\n        )\n        return worker\n\n    def test_heartbeat_stops_server_when_not_alive(self):\n        \"\"\"Test that heartbeat stops the server when not alive.\"\"\"\n        worker = self.create_worker()\n        worker.alive = False\n        worker.server_alive = True\n        worker.server = mock.Mock()\n\n        worker.heartbeat()\n\n        worker.server.stop.assert_called_once()\n        assert worker.server_alive is False\n\n    def test_heartbeat_stops_ioloop_after_server(self):\n        \"\"\"Test that heartbeat stops IOLoop after server is stopped.\"\"\"\n        worker = self.create_worker()\n        worker.alive = False\n        worker.server_alive = False\n        worker.callbacks = [mock.Mock(), mock.Mock()]\n        worker.ioloop = mock.Mock()\n\n        worker.heartbeat()\n\n        for callback in worker.callbacks:\n            callback.stop.assert_called_once()\n        worker.ioloop.stop.assert_called_once()\n\n\nclass TestAppWrapping:\n    \"\"\"Tests for app wrapping logic.\"\"\"\n\n    def create_worker(self):\n        \"\"\"Create a worker for testing.\"\"\"\n        cfg = Config()\n        cfg.set('workers', 1)\n\n        worker = gtornado.TornadoWorker(\n            age=1,\n            ppid=os.getpid(),\n            sockets=[],\n            app=mock.Mock(),\n            timeout=30,\n            cfg=cfg,\n            log=mock.Mock(),\n        )\n        return worker\n\n    def test_wsgi_callable_wrapped_in_container(self):\n        \"\"\"Test that a plain WSGI callable gets wrapped in WSGIContainer.\"\"\"\n        from tornado.wsgi import WSGIContainer\n\n        def wsgi_app(environ, start_response):\n            pass\n\n        # Test that WSGIContainer is used for plain WSGI apps\n        app = wsgi_app\n        if not isinstance(app, WSGIContainer) and \\\n                not isinstance(app, tornado.web.Application):\n            app = WSGIContainer(app)\n\n        assert isinstance(app, WSGIContainer)\n\n    def test_tornado_application_not_wrapped(self):\n        \"\"\"Test that tornado.web.Application is not wrapped.\"\"\"\n        from tornado.wsgi import WSGIContainer\n\n        tornado_app = tornado.web.Application([])\n\n        # Test the wrapping logic\n        app = tornado_app\n        if not isinstance(app, WSGIContainer) and \\\n                not isinstance(app, tornado.web.Application):\n            app = WSGIContainer(app)\n\n        # Should NOT be wrapped\n        assert isinstance(app, tornado.web.Application)\n        assert not isinstance(app, WSGIContainer)\n\n\nclass TestSetup:\n    \"\"\"Tests for the setup class method.\"\"\"\n\n    def test_setup_patches_request_handler(self):\n        \"\"\"Test that setup patches RequestHandler.clear.\"\"\"\n        # Save original\n        original_clear = tornado.web.RequestHandler.clear\n\n        try:\n            gtornado.TornadoWorker.setup()\n\n            # Create a mock handler to test the patched clear method\n            mock_handler = mock.Mock()\n            mock_handler._headers = {\"Server\": \"TornadoServer/1.0\"}\n\n            # Call the patched clear\n            new_clear = tornado.web.RequestHandler.clear\n            assert new_clear is not original_clear\n\n        finally:\n            # Restore original\n            tornado.web.RequestHandler.clear = original_clear\n\n\nclass TestRunMethod:\n    \"\"\"Tests for the run method.\"\"\"\n\n    def create_worker(self):\n        \"\"\"Create a worker for testing.\"\"\"\n        cfg = Config()\n        cfg.set('workers', 1)\n        cfg.set('keepalive', 2)\n\n        worker = gtornado.TornadoWorker(\n            age=1,\n            ppid=os.getpid(),\n            sockets=[],\n            app=mock.Mock(),\n            timeout=30,\n            cfg=cfg,\n            log=mock.Mock(),\n        )\n        return worker\n\n    def test_run_sets_up_callbacks(self):\n        \"\"\"Test that run sets up periodic callbacks.\"\"\"\n        worker = self.create_worker()\n        worker.wsgi = tornado.web.Application([])\n        worker.sockets = []\n\n        mock_ioloop = mock.Mock()\n        mock_callback = mock.Mock()\n\n        with mock.patch.object(gtornado.IOLoop, 'instance', return_value=mock_ioloop):\n            with mock.patch.object(gtornado, 'PeriodicCallback', return_value=mock_callback) as mock_pc:\n                # Start the run method but stop it immediately\n                mock_ioloop.start.side_effect = lambda: None\n\n                worker.run()\n\n                # Should create two callbacks (watchdog and heartbeat)\n                assert mock_pc.call_count == 2\n                assert mock_callback.start.call_count == 2\n\n    def test_run_creates_http_server(self):\n        \"\"\"Test that run creates an HTTP server.\"\"\"\n        worker = self.create_worker()\n        worker.wsgi = tornado.web.Application([])\n        worker.sockets = []\n\n        mock_ioloop = mock.Mock()\n        mock_ioloop.start.side_effect = lambda: None\n\n        with mock.patch.object(gtornado.IOLoop, 'instance', return_value=mock_ioloop):\n            with mock.patch.object(gtornado, 'PeriodicCallback', return_value=mock.Mock()):\n                worker.run()\n\n                assert worker.server is not None\n                assert worker.server_alive is True\n\n    def test_run_adds_sockets_to_server(self):\n        \"\"\"Test that run adds sockets to the server.\"\"\"\n        worker = self.create_worker()\n        worker.wsgi = tornado.web.Application([])\n\n        mock_socket = FakeSocket()\n        worker.sockets = [mock_socket]\n\n        mock_ioloop = mock.Mock()\n        mock_ioloop.start.side_effect = lambda: None\n\n        with mock.patch.object(gtornado.IOLoop, 'instance', return_value=mock_ioloop):\n            with mock.patch.object(gtornado, 'PeriodicCallback', return_value=mock.Mock()):\n                with mock.patch.object(tornado.httpserver.HTTPServer, 'add_socket'):\n                    worker.run()\n\n                    # Socket should be set to non-blocking (setblocking(0))\n                    assert not mock_socket.blocking\n\n\nclass TestSSLSupport:\n    \"\"\"Tests for SSL support.\"\"\"\n\n    def create_worker(self):\n        \"\"\"Create a worker for testing.\"\"\"\n        cfg = Config()\n        cfg.set('workers', 1)\n        cfg.set('keepalive', 2)\n\n        worker = gtornado.TornadoWorker(\n            age=1,\n            ppid=os.getpid(),\n            sockets=[],\n            app=mock.Mock(),\n            timeout=30,\n            cfg=cfg,\n            log=mock.Mock(),\n        )\n        return worker\n\n    def test_ssl_server_creation(self):\n        \"\"\"Test that SSL server is created when is_ssl is True.\"\"\"\n        worker = self.create_worker()\n        worker.wsgi = tornado.web.Application([])\n        worker.sockets = []\n\n        mock_ioloop = mock.Mock()\n        mock_ioloop.start.side_effect = lambda: None\n\n        mock_ssl_context = mock.Mock()\n\n        # Mock cfg.is_ssl property to return True\n        with mock.patch.object(type(worker.cfg), 'is_ssl', new_callable=mock.PropertyMock, return_value=True):\n            with mock.patch.object(gtornado.IOLoop, 'instance', return_value=mock_ioloop):\n                with mock.patch.object(gtornado, 'PeriodicCallback', return_value=mock.Mock()):\n                    with mock.patch.object(gtornado, 'ssl_context', return_value=mock_ssl_context):\n                        worker.run()\n\n                        # Server should be created with ssl_options\n                        assert worker.server is not None\n\n\nclass TestKeepAlive:\n    \"\"\"Tests for keep-alive configuration.\"\"\"\n\n    def create_worker(self):\n        \"\"\"Create a worker for testing.\"\"\"\n        cfg = Config()\n        cfg.set('workers', 1)\n\n        worker = gtornado.TornadoWorker(\n            age=1,\n            ppid=os.getpid(),\n            sockets=[],\n            app=mock.Mock(),\n            timeout=30,\n            cfg=cfg,\n            log=mock.Mock(),\n        )\n        return worker\n\n    def test_keep_alive_enabled(self):\n        \"\"\"Test that keep-alive is enabled when keepalive > 0.\"\"\"\n        worker = self.create_worker()\n        worker.wsgi = tornado.web.Application([])\n        worker.cfg.set('keepalive', 2)\n        worker.sockets = []\n\n        mock_ioloop = mock.Mock()\n        mock_ioloop.start.side_effect = lambda: None\n\n        with mock.patch.object(gtornado.IOLoop, 'instance', return_value=mock_ioloop):\n            with mock.patch.object(gtornado, 'PeriodicCallback', return_value=mock.Mock()):\n                worker.run()\n\n                assert worker.server.no_keep_alive is False\n\n    def test_keep_alive_disabled(self):\n        \"\"\"Test that keep-alive is disabled when keepalive <= 0.\"\"\"\n        worker = self.create_worker()\n        worker.wsgi = tornado.web.Application([])\n        worker.cfg.set('keepalive', 0)\n        worker.sockets = []\n\n        mock_ioloop = mock.Mock()\n        mock_ioloop.start.side_effect = lambda: None\n\n        with mock.patch.object(gtornado.IOLoop, 'instance', return_value=mock_ioloop):\n            with mock.patch.object(gtornado, 'PeriodicCallback', return_value=mock.Mock()):\n                worker.run()\n\n                assert worker.server.no_keep_alive is True\n"
  },
  {
    "path": "tests/test_http.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport io\nimport t\nimport pytest\nfrom unittest import mock\n\nfrom gunicorn import util\nfrom gunicorn.http.body import Body, LengthReader, EOFReader\nfrom gunicorn.http.wsgi import Response\nfrom gunicorn.http.unreader import Unreader, IterUnreader, SocketUnreader\nfrom gunicorn.http.errors import InvalidHeader, InvalidHeaderName, InvalidHTTPVersion\nfrom gunicorn.http.message import TOKEN_RE\n\n\ndef test_method_pattern():\n    assert TOKEN_RE.fullmatch(\"GET\")\n    assert TOKEN_RE.fullmatch(\"MKCALENDAR\")\n    assert not TOKEN_RE.fullmatch(\"GET:\")\n    assert not TOKEN_RE.fullmatch(\"GET;\")\n    RFC9110_5_6_2_TOKEN_DELIM = r'\"(),/:;<=>?@[\\]{}'\n    for bad_char in RFC9110_5_6_2_TOKEN_DELIM:\n        assert not TOKEN_RE.match(bad_char)\n\n\ndef assert_readline(payload, size, expected):\n    body = Body(io.BytesIO(payload))\n    assert body.readline(size) == expected\n\n\ndef test_readline_empty_body():\n    assert_readline(b\"\", None, b\"\")\n    assert_readline(b\"\", 1, b\"\")\n\n\ndef test_readline_zero_size():\n    assert_readline(b\"abc\", 0, b\"\")\n    assert_readline(b\"\\n\", 0, b\"\")\n\n\ndef test_readline_new_line_before_size():\n    body = Body(io.BytesIO(b\"abc\\ndef\"))\n    assert body.readline(4) == b\"abc\\n\"\n    assert body.readline() == b\"def\"\n\n\ndef test_readline_new_line_after_size():\n    body = Body(io.BytesIO(b\"abc\\ndef\"))\n    assert body.readline(2) == b\"ab\"\n    assert body.readline() == b\"c\\n\"\n\n\ndef test_readline_no_new_line():\n    body = Body(io.BytesIO(b\"abcdef\"))\n    assert body.readline() == b\"abcdef\"\n    body = Body(io.BytesIO(b\"abcdef\"))\n    assert body.readline(2) == b\"ab\"\n    assert body.readline(2) == b\"cd\"\n    assert body.readline(2) == b\"ef\"\n\n\ndef test_readline_buffer_loaded():\n    reader = io.BytesIO(b\"abc\\ndef\")\n    body = Body(reader)\n    body.read(1) # load internal buffer\n    reader.write(b\"g\\nhi\")\n    reader.seek(7)\n    assert body.readline() == b\"bc\\n\"\n    assert body.readline() == b\"defg\\n\"\n    assert body.readline() == b\"hi\"\n\n\ndef test_readline_buffer_loaded_with_size():\n    body = Body(io.BytesIO(b\"abc\\ndef\"))\n    body.read(1)  # load internal buffer\n    assert body.readline(2) == b\"bc\"\n    assert body.readline(2) == b\"\\n\"\n    assert body.readline(2) == b\"de\"\n    assert body.readline(2) == b\"f\"\n\n\ndef test_http_header_encoding():\n    \"\"\" tests whether http response headers are USASCII encoded \"\"\"\n\n    mocked_socket = mock.MagicMock()\n    mocked_socket.sendall = mock.MagicMock()\n\n    mocked_request = mock.MagicMock()\n    response = Response(mocked_request, mocked_socket, None)\n\n    # set umlaut header value - latin-1 is OK\n    response.headers.append(('foo', 'häder'))\n    response.send_headers()\n\n    # set a-breve header value - unicode, non-latin-1 fails\n    response = Response(mocked_request, mocked_socket, None)\n    response.headers.append(('apple', 'măr'))\n    with pytest.raises(UnicodeEncodeError):\n        response.send_headers()\n\n    # build our own header_str to compare against\n    tosend = response.default_headers()\n    tosend.extend([\"%s: %s\\r\\n\" % (k, v) for k, v in response.headers])\n    header_str = \"%s\\r\\n\" % \"\".join(tosend)\n\n    with pytest.raises(UnicodeEncodeError):\n        mocked_socket.sendall(util.to_bytestring(header_str, \"ascii\"))\n\n\ndef test_http_invalid_response_header():\n    \"\"\" tests whether http response headers are contains control chars \"\"\"\n\n    mocked_socket = mock.MagicMock()\n    mocked_socket.sendall = mock.MagicMock()\n\n    mocked_request = mock.MagicMock()\n    response = Response(mocked_request, mocked_socket, None)\n\n    with pytest.raises(InvalidHeader):\n        response.start_response(\"200 OK\", [('foo', 'essai\\r\\n')])\n\n    response = Response(mocked_request, mocked_socket, None)\n    with pytest.raises(InvalidHeaderName):\n        response.start_response(\"200 OK\", [('foo\\r\\n', 'essai')])\n\n\ndef test_unreader_read_when_size_is_none():\n    unreader = Unreader()\n    unreader.chunk = mock.MagicMock(side_effect=[b'qwerty', b'123456', b''])\n\n    assert unreader.read(size=None) == b'qwerty'\n    assert unreader.read(size=None) == b'123456'\n    assert unreader.read(size=None) == b''\n\n\ndef test_unreader_unread():\n    unreader = Unreader()\n    unreader.unread(b'hi there')\n    assert b'hi there' in unreader.read()\n\n\ndef test_unreader_unread_should_place_data_at_the_beginning_of_the_buffer():\n    unreader = IterUnreader([b\"abc\", b\"def\"])\n    ab = unreader.read(2)\n    unreader.unread(ab)\n\n    assert unreader.read(None) == b\"abc\"\n\n\ndef test_unreader_read_zero_size():\n    unreader = Unreader()\n    unreader.chunk = mock.MagicMock(side_effect=[b'qwerty', b'asdfgh'])\n\n    assert unreader.read(size=0) == b''\n\n\ndef test_unreader_read_with_nonzero_size():\n    unreader = Unreader()\n    unreader.chunk = mock.MagicMock(side_effect=[\n        b'qwerty', b'asdfgh', b'zxcvbn', b'123456', b'', b''\n    ])\n\n    assert unreader.read(size=5) == b'qwert'\n    assert unreader.read(size=5) == b'yasdf'\n    assert unreader.read(size=5) == b'ghzxc'\n    assert unreader.read(size=5) == b'vbn12'\n    assert unreader.read(size=5) == b'3456'\n    assert unreader.read(size=5) == b''\n\n\ndef test_unreader_raises_excpetion_on_invalid_size():\n    unreader = Unreader()\n    with pytest.raises(TypeError):\n        unreader.read(size='foobar')\n    with pytest.raises(TypeError):\n        unreader.read(size=3.14)\n    with pytest.raises(TypeError):\n        unreader.read(size=[])\n\n\ndef test_iter_unreader_chunk():\n    iter_unreader = IterUnreader((b'ab', b'cd', b'ef'))\n\n    assert iter_unreader.chunk() == b'ab'\n    assert iter_unreader.chunk() == b'cd'\n    assert iter_unreader.chunk() == b'ef'\n    assert iter_unreader.chunk() == b''\n    assert iter_unreader.chunk() == b''\n\n\ndef test_socket_unreader_chunk():\n    fake_sock = t.FakeSocket(io.BytesIO(b'Lorem ipsum dolor'))\n    sock_unreader = SocketUnreader(fake_sock, max_chunk=5)\n\n    assert sock_unreader.chunk() == b'Lorem'\n    assert sock_unreader.chunk() == b' ipsu'\n    assert sock_unreader.chunk() == b'm dol'\n    assert sock_unreader.chunk() == b'or'\n    assert sock_unreader.chunk() == b''\n\n\ndef test_length_reader_read():\n    unreader = IterUnreader((b'Lorem', b'ipsum', b'dolor', b'sit', b'amet'))\n    reader = LengthReader(unreader, 13)\n    assert reader.read(0) == b''\n    assert reader.read(5) == b'Lorem'\n    assert reader.read(6) == b'ipsumd'\n    assert reader.read(4) == b'ol'\n    assert reader.read(100) == b''\n\n    reader = LengthReader(unreader, 10)\n    assert reader.read(0) == b''\n    assert reader.read(5) == b'orsit'\n    assert reader.read(5) == b'amet'\n    assert reader.read(100) == b''\n\n\ndef test_length_reader_read_invalid_size():\n    reader = LengthReader(None, 5)\n    with pytest.raises(TypeError):\n        reader.read('100')\n    with pytest.raises(TypeError):\n        reader.read([100])\n    with pytest.raises(ValueError):\n        reader.read(-100)\n\n\ndef test_eof_reader_read():\n    unreader = IterUnreader((b'Lorem', b'ipsum', b'dolor', b'sit', b'amet'))\n    reader = EOFReader(unreader)\n\n    assert reader.read(0) == b''\n    assert reader.read(5) == b'Lorem'\n    assert reader.read(5) == b'ipsum'\n    assert reader.read(3) == b'dol'\n    assert reader.read(3) == b'ors'\n    assert reader.read(100) == b'itamet'\n    assert reader.read(100) == b''\n\n\ndef test_eof_reader_read_invalid_size():\n    reader = EOFReader(None)\n    with pytest.raises(TypeError):\n        reader.read('100')\n    with pytest.raises(TypeError):\n        reader.read([100])\n    with pytest.raises(ValueError):\n        reader.read(-100)\n\n\ndef test_invalid_http_version_error():\n    assert str(InvalidHTTPVersion('foo')) == \"Invalid HTTP Version: 'foo'\"\n    assert str(InvalidHTTPVersion((2, 1))) == 'Invalid HTTP Version: (2, 1)'\n"
  },
  {
    "path": "tests/test_http2_alpn.py",
    "content": "# -*- coding: utf-8 -\n#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Tests for HTTP/2 ALPN negotiation.\"\"\"\n\nimport ssl\nimport pytest\nfrom unittest import mock\n\nfrom gunicorn import sock\n\n\ndef create_mock_ssl_socket(alpn_protocol=None):\n    \"\"\"Create a mock SSL socket for testing ALPN negotiation.\"\"\"\n    mock_socket = mock.Mock(spec=ssl.SSLSocket)\n    mock_socket.selected_alpn_protocol.return_value = alpn_protocol\n    return mock_socket\n\n\nclass TestGetAlpnProtocols:\n    \"\"\"Test _get_alpn_protocols function.\"\"\"\n\n    def test_h1_only_returns_empty(self):\n        \"\"\"No ALPN needed for HTTP/1.1 only.\"\"\"\n        conf = mock.Mock()\n        conf.http_protocols = [\"h1\"]\n\n        result = sock._get_alpn_protocols(conf)\n        assert result == []\n\n    def test_h2_enabled_returns_alpn_list(self):\n        \"\"\"Should return ALPN protocols when h2 is enabled.\"\"\"\n        conf = mock.Mock()\n        conf.http_protocols = [\"h2\", \"h1\"]\n\n        with mock.patch('gunicorn.http2.is_http2_available', return_value=True):\n            result = sock._get_alpn_protocols(conf)\n            assert \"h2\" in result\n            assert \"http/1.1\" in result\n\n    def test_h2_without_library_returns_empty(self):\n        \"\"\"Should return empty if h2 library not available.\"\"\"\n        conf = mock.Mock()\n        conf.http_protocols = [\"h2\", \"h1\"]\n\n        with mock.patch('gunicorn.http2.is_http2_available', return_value=False):\n            result = sock._get_alpn_protocols(conf)\n            assert result == []\n\n    def test_empty_protocols_returns_empty(self):\n        conf = mock.Mock()\n        conf.http_protocols = []\n\n        result = sock._get_alpn_protocols(conf)\n        assert result == []\n\n    def test_none_protocols_returns_empty(self):\n        conf = mock.Mock()\n        conf.http_protocols = None\n\n        result = sock._get_alpn_protocols(conf)\n        assert result == []\n\n    def test_h2_only(self):\n        \"\"\"Should work with h2 only.\"\"\"\n        conf = mock.Mock()\n        conf.http_protocols = [\"h2\"]\n\n        with mock.patch('gunicorn.http2.is_http2_available', return_value=True):\n            result = sock._get_alpn_protocols(conf)\n            assert \"h2\" in result\n\n\nclass TestGetNegotiatedProtocol:\n    \"\"\"Test get_negotiated_protocol function.\"\"\"\n\n    def test_returns_alpn_protocol(self):\n        ssl_socket = create_mock_ssl_socket(alpn_protocol=\"h2\")\n        result = sock.get_negotiated_protocol(ssl_socket)\n        assert result == \"h2\"\n\n    def test_returns_http11(self):\n        ssl_socket = create_mock_ssl_socket(alpn_protocol=\"http/1.1\")\n        result = sock.get_negotiated_protocol(ssl_socket)\n        assert result == \"http/1.1\"\n\n    def test_returns_none_when_not_negotiated(self):\n        ssl_socket = create_mock_ssl_socket(alpn_protocol=None)\n        result = sock.get_negotiated_protocol(ssl_socket)\n        assert result is None\n\n    def test_returns_none_for_non_ssl_socket(self):\n        regular_socket = mock.Mock(spec=[])  # No SSL methods\n        result = sock.get_negotiated_protocol(regular_socket)\n        assert result is None\n\n    def test_handles_attribute_error(self):\n        \"\"\"Handle old SSL without selected_alpn_protocol.\"\"\"\n        ssl_socket = mock.Mock(spec=ssl.SSLSocket)\n        del ssl_socket.selected_alpn_protocol  # Remove the method\n        result = sock.get_negotiated_protocol(ssl_socket)\n        assert result is None\n\n    def test_handles_ssl_error(self):\n        \"\"\"Handle SSLError when checking protocol.\"\"\"\n        ssl_socket = mock.Mock(spec=ssl.SSLSocket)\n        ssl_socket.selected_alpn_protocol.side_effect = ssl.SSLError()\n        result = sock.get_negotiated_protocol(ssl_socket)\n        assert result is None\n\n\nclass TestIsHttp2Negotiated:\n    \"\"\"Test is_http2_negotiated function.\"\"\"\n\n    def test_returns_true_for_h2(self):\n        ssl_socket = create_mock_ssl_socket(alpn_protocol=\"h2\")\n        result = sock.is_http2_negotiated(ssl_socket)\n        assert result is True\n\n    def test_returns_false_for_http11(self):\n        ssl_socket = create_mock_ssl_socket(alpn_protocol=\"http/1.1\")\n        result = sock.is_http2_negotiated(ssl_socket)\n        assert result is False\n\n    def test_returns_false_for_none(self):\n        ssl_socket = create_mock_ssl_socket(alpn_protocol=None)\n        result = sock.is_http2_negotiated(ssl_socket)\n        assert result is False\n\n    def test_returns_false_for_non_ssl(self):\n        regular_socket = mock.Mock(spec=[])\n        result = sock.is_http2_negotiated(regular_socket)\n        assert result is False\n\n\nclass TestSSLContextAlpnConfiguration:\n    \"\"\"Test that SSL context configures ALPN properly.\"\"\"\n\n    @pytest.fixture\n    def ssl_config(self, tmp_path):\n        \"\"\"Create a config with SSL settings.\"\"\"\n        # Create dummy cert/key files\n        certfile = tmp_path / \"cert.pem\"\n        keyfile = tmp_path / \"key.pem\"\n        certfile.touch()\n        keyfile.touch()\n\n        conf = mock.Mock()\n        conf.certfile = str(certfile)\n        conf.keyfile = str(keyfile)\n        conf.ca_certs = None\n        conf.cert_reqs = ssl.CERT_NONE\n        conf.ciphers = None\n        conf.http_protocols = [\"h2\", \"h1\"]\n        conf.ssl_context = lambda conf, factory: factory()\n\n        return conf\n\n    def test_ssl_context_sets_alpn_when_h2_available(self, ssl_config):\n        \"\"\"SSL context should set ALPN protocols when h2 is available.\"\"\"\n        with mock.patch('gunicorn.http2.is_http2_available', return_value=True):\n            with mock.patch('ssl.create_default_context') as mock_ctx:\n                mock_context = mock.Mock()\n                mock_ctx.return_value = mock_context\n                mock_context.load_cert_chain = mock.Mock()\n\n                try:\n                    sock.ssl_context(ssl_config)\n                except Exception:\n                    pass  # May fail due to dummy certs\n\n                # Check that set_alpn_protocols was called\n                if mock_context.set_alpn_protocols.called:\n                    call_args = mock_context.set_alpn_protocols.call_args[0][0]\n                    assert 'h2' in call_args\n\n    def test_ssl_context_no_alpn_when_h1_only(self):\n        \"\"\"SSL context should not set ALPN for HTTP/1.1 only.\"\"\"\n        conf = mock.Mock()\n        conf.http_protocols = [\"h1\"]\n        conf.ca_certs = None\n        conf.certfile = \"cert.pem\"\n        conf.keyfile = \"key.pem\"\n        conf.cert_reqs = ssl.CERT_NONE\n        conf.ciphers = None\n        conf.ssl_context = lambda conf, factory: factory()\n\n        with mock.patch('ssl.create_default_context') as mock_ctx:\n            mock_context = mock.Mock()\n            mock_ctx.return_value = mock_context\n\n            # ALPN should not be set for h1 only\n            alpn_protocols = sock._get_alpn_protocols(conf)\n            assert alpn_protocols == []\n\n\nclass TestAlpnProtocolMap:\n    \"\"\"Test ALPN protocol mapping.\"\"\"\n\n    def test_h1_maps_to_http11(self):\n        from gunicorn.config import ALPN_PROTOCOL_MAP\n        assert ALPN_PROTOCOL_MAP.get(\"h1\") == \"http/1.1\"\n\n    def test_h2_maps_to_h2(self):\n        from gunicorn.config import ALPN_PROTOCOL_MAP\n        assert ALPN_PROTOCOL_MAP.get(\"h2\") == \"h2\"\n\n\nclass TestAsyncWorkerAlpnHandshake:\n    \"\"\"Test that AsyncWorker performs handshake before ALPN check.\n\n    This is critical for gevent and eventlet workers where do_handshake_on_connect\n    may be False, causing ALPN negotiation to not complete until first I/O.\n    \"\"\"\n\n    @pytest.fixture\n    def async_worker(self):\n        \"\"\"Create an AsyncWorker instance for testing.\"\"\"\n        from gunicorn.workers.base_async import AsyncWorker\n\n        worker = AsyncWorker.__new__(AsyncWorker)\n        worker.cfg = mock.MagicMock()\n        worker.cfg.keepalive = 2\n        worker.cfg.do_handshake_on_connect = False\n        worker.cfg.http_protocols = [\"h2\", \"h1\"]\n        worker.alive = True\n        worker.log = mock.MagicMock()\n        worker.wsgi = mock.MagicMock()\n        worker.nr = 0\n        worker.max_requests = 1000\n\n        return worker\n\n    def test_handshake_called_when_do_handshake_on_connect_false(self, async_worker):\n        \"\"\"Test that do_handshake() is called when do_handshake_on_connect is False.\"\"\"\n        mock_ssl_socket = mock.Mock(spec=ssl.SSLSocket)\n        mock_ssl_socket.selected_alpn_protocol.return_value = None\n        mock_listener = mock.MagicMock()\n\n        # Mock the rest of handle() to prevent full execution\n        with mock.patch('gunicorn.sock.is_http2_negotiated', return_value=False):\n            with mock.patch('gunicorn.http.get_parser') as mock_parser:\n                mock_parser.return_value = iter([])\n                try:\n                    async_worker.handle(mock_listener, mock_ssl_socket, ('127.0.0.1', 8000))\n                except StopIteration:\n                    pass\n\n        # Verify handshake was called\n        mock_ssl_socket.do_handshake.assert_called_once()\n\n    def test_no_handshake_when_do_handshake_on_connect_true(self, async_worker):\n        \"\"\"Test that do_handshake() is NOT called when do_handshake_on_connect is True.\"\"\"\n        async_worker.cfg.do_handshake_on_connect = True\n\n        mock_ssl_socket = mock.Mock(spec=ssl.SSLSocket)\n        mock_ssl_socket.selected_alpn_protocol.return_value = None\n        mock_listener = mock.MagicMock()\n\n        with mock.patch('gunicorn.sock.is_http2_negotiated', return_value=False):\n            with mock.patch('gunicorn.http.get_parser') as mock_parser:\n                mock_parser.return_value = iter([])\n                try:\n                    async_worker.handle(mock_listener, mock_ssl_socket, ('127.0.0.1', 8000))\n                except StopIteration:\n                    pass\n\n        # Verify handshake was NOT called (already done on connect)\n        mock_ssl_socket.do_handshake.assert_not_called()\n\n    def test_no_handshake_for_non_ssl_socket(self, async_worker):\n        \"\"\"Test that no handshake is attempted for non-SSL sockets.\"\"\"\n        mock_socket = mock.MagicMock()  # Regular socket, not ssl.SSLSocket\n        mock_listener = mock.MagicMock()\n\n        with mock.patch('gunicorn.sock.is_http2_negotiated', return_value=False):\n            with mock.patch('gunicorn.http.get_parser') as mock_parser:\n                mock_parser.return_value = iter([])\n                try:\n                    async_worker.handle(mock_listener, mock_socket, ('127.0.0.1', 8000))\n                except StopIteration:\n                    pass\n\n        # Non-SSL sockets don't have do_handshake, so it shouldn't be called\n        assert not hasattr(mock_socket, 'do_handshake') or \\\n               not mock_socket.do_handshake.called\n\n    def test_http2_detected_after_handshake(self, async_worker):\n        \"\"\"Test that HTTP/2 is properly detected after explicit handshake.\"\"\"\n        mock_ssl_socket = mock.Mock(spec=ssl.SSLSocket)\n        mock_ssl_socket.selected_alpn_protocol.return_value = \"h2\"\n        mock_listener = mock.MagicMock()\n\n        with mock.patch.object(async_worker, 'handle_http2') as mock_h2:\n            async_worker.handle(mock_listener, mock_ssl_socket, ('127.0.0.1', 8000))\n\n        # Verify handshake was called first\n        mock_ssl_socket.do_handshake.assert_called_once()\n        # Verify HTTP/2 handler was invoked\n        mock_h2.assert_called_once()\n\n\nclass TestGeventWorkerAlpn:\n    \"\"\"Test ALPN handling in GeventWorker.\"\"\"\n\n    @pytest.fixture\n    def gevent_worker(self):\n        \"\"\"Create a GeventWorker instance for testing.\"\"\"\n        try:\n            import gevent\n        except ImportError:\n            pytest.skip(\"gevent not available\")\n\n        from gunicorn.workers.ggevent import GeventWorker\n\n        worker = GeventWorker.__new__(GeventWorker)\n        worker.cfg = mock.MagicMock()\n        worker.cfg.keepalive = 2\n        worker.cfg.do_handshake_on_connect = False\n        worker.cfg.http_protocols = [\"h2\", \"h1\"]\n        worker.cfg.is_ssl = True\n        worker.alive = True\n        worker.log = mock.MagicMock()\n        worker.wsgi = mock.MagicMock()\n        worker.nr = 0\n        worker.max_requests = 1000\n        worker.worker_connections = 1000\n\n        return worker\n\n    def test_gevent_inherits_async_worker(self):\n        \"\"\"Test that GeventWorker inherits from AsyncWorker.\"\"\"\n        try:\n            import gevent\n        except ImportError:\n            pytest.skip(\"gevent not available\")\n\n        from gunicorn.workers.ggevent import GeventWorker\n        from gunicorn.workers.base_async import AsyncWorker\n\n        assert issubclass(GeventWorker, AsyncWorker)\n\n    def test_gevent_handle_calls_super(self, gevent_worker):\n        \"\"\"Test that GeventWorker.handle() calls super().handle().\"\"\"\n        mock_client = mock.MagicMock()\n        mock_listener = mock.MagicMock()\n\n        with mock.patch('gunicorn.workers.base_async.AsyncWorker.handle') as mock_super:\n            gevent_worker.handle(mock_listener, mock_client, ('127.0.0.1', 8000))\n\n        mock_super.assert_called_once()\n\n\nclass TestEventletWorkerAlpn:\n    \"\"\"Test ALPN handling in EventletWorker.\"\"\"\n\n    @pytest.fixture\n    def eventlet_worker(self):\n        \"\"\"Create an EventletWorker instance for testing.\"\"\"\n        try:\n            import eventlet\n        except (ImportError, AttributeError):\n            pytest.skip(\"eventlet not available\")\n\n        from gunicorn.workers.geventlet import EventletWorker\n\n        worker = EventletWorker.__new__(EventletWorker)\n        worker.cfg = mock.MagicMock()\n        worker.cfg.keepalive = 2\n        worker.cfg.do_handshake_on_connect = False\n        worker.cfg.http_protocols = [\"h2\", \"h1\"]\n        worker.cfg.is_ssl = True\n        worker.alive = True\n        worker.log = mock.MagicMock()\n        worker.wsgi = mock.MagicMock()\n        worker.nr = 0\n        worker.max_requests = 1000\n        worker.worker_connections = 1000\n\n        return worker\n\n    def test_eventlet_inherits_async_worker(self):\n        \"\"\"Test that EventletWorker inherits from AsyncWorker.\"\"\"\n        try:\n            import eventlet\n        except (ImportError, AttributeError):\n            pytest.skip(\"eventlet not available\")\n\n        from gunicorn.workers.geventlet import EventletWorker\n        from gunicorn.workers.base_async import AsyncWorker\n\n        assert issubclass(EventletWorker, AsyncWorker)\n\n    def test_eventlet_handle_wraps_ssl_then_calls_super(self, eventlet_worker):\n        \"\"\"Test that EventletWorker.handle() wraps SSL then calls super().\"\"\"\n        from gunicorn.workers import geventlet\n\n        mock_client = mock.MagicMock()\n        mock_wrapped = mock.MagicMock()\n        mock_listener = mock.MagicMock()\n\n        with mock.patch.object(geventlet, 'ssl_wrap_socket', return_value=mock_wrapped):\n            with mock.patch('gunicorn.workers.base_async.AsyncWorker.handle') as mock_super:\n                eventlet_worker.handle(mock_listener, mock_client, ('127.0.0.1', 8000))\n\n        # Verify super().handle() was called with the wrapped socket\n        mock_super.assert_called_once()\n        call_args = mock_super.call_args[0]\n        assert call_args[1] == mock_wrapped  # Second arg is the client socket\n\n    def test_eventlet_alpn_works_with_handshake_fix(self, eventlet_worker):\n        \"\"\"Test that ALPN detection works after handshake fix for eventlet.\"\"\"\n        from gunicorn.workers import geventlet\n\n        mock_ssl_socket = mock.Mock(spec=ssl.SSLSocket)\n        mock_ssl_socket.selected_alpn_protocol.return_value = \"h2\"\n        mock_listener = mock.MagicMock()\n\n        with mock.patch.object(geventlet, 'ssl_wrap_socket', return_value=mock_ssl_socket):\n            with mock.patch.object(eventlet_worker, 'handle_http2') as mock_h2:\n                eventlet_worker.handle(mock_listener, mock.MagicMock(), ('127.0.0.1', 8000))\n\n        # Verify handshake was called (by base_async.handle)\n        mock_ssl_socket.do_handshake.assert_called_once()\n        # Verify HTTP/2 handler was invoked\n        mock_h2.assert_called_once()\n"
  },
  {
    "path": "tests/test_http2_async_connection.py",
    "content": "# -*- coding: utf-8 -\n#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Tests for async HTTP/2 server connection.\"\"\"\n\nimport asyncio\nimport pytest\nfrom unittest import mock\nfrom io import BytesIO\n\n# Check if h2 is available for integration tests\ntry:\n    import h2.connection\n    import h2.config\n    import h2.events\n    H2_AVAILABLE = True\nexcept ImportError:\n    H2_AVAILABLE = False\n\nfrom gunicorn.http2.errors import (\n    HTTP2Error, HTTP2ConnectionError\n)\n\n\npytestmark = pytest.mark.skipif(not H2_AVAILABLE, reason=\"h2 library not available\")\n\n\nclass MockConfig:\n    \"\"\"Mock gunicorn configuration for HTTP/2.\"\"\"\n\n    def __init__(self):\n        self.http2_max_concurrent_streams = 100\n        self.http2_initial_window_size = 65535\n        self.http2_max_frame_size = 16384\n        self.http2_max_header_list_size = 65536\n\n\nclass MockAsyncReader:\n    \"\"\"Mock asyncio StreamReader for testing.\"\"\"\n\n    def __init__(self, data=b''):\n        self._buffer = BytesIO(data)\n        self._eof = False\n\n    async def read(self, n=-1):\n        data = self._buffer.read(n)\n        if not data and self._eof:\n            return b''\n        return data\n\n    def set_data(self, data):\n        self._buffer = BytesIO(data)\n\n    def set_eof(self):\n        self._eof = True\n        self._buffer = BytesIO(b'')\n\n\nclass MockAsyncWriter:\n    \"\"\"Mock asyncio StreamWriter for testing.\"\"\"\n\n    def __init__(self):\n        self._buffer = bytearray()\n        self._closed = False\n        self._drained = False\n\n    def write(self, data):\n        if self._closed:\n            raise OSError(\"Writer is closed\")\n        self._buffer.extend(data)\n\n    async def drain(self):\n        self._drained = True\n\n    def close(self):\n        self._closed = True\n\n    async def wait_closed(self):\n        pass\n\n    def get_written_data(self):\n        return bytes(self._buffer)\n\n    def clear(self):\n        self._buffer.clear()\n\n\ndef create_client_connection():\n    \"\"\"Create an h2 client connection for generating test frames.\"\"\"\n    config = h2.config.H2Configuration(client_side=True)\n    conn = h2.connection.H2Connection(config=config)\n    conn.initiate_connection()\n    return conn\n\n\nclass TestAsyncHTTP2ConnectionInit:\n    \"\"\"Test AsyncHTTP2Connection initialization.\"\"\"\n\n    def test_basic_initialization(self):\n        from gunicorn.http2.async_connection import AsyncHTTP2Connection\n\n        cfg = MockConfig()\n        reader = MockAsyncReader()\n        writer = MockAsyncWriter()\n        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))\n\n        assert conn.cfg is cfg\n        assert conn.reader is reader\n        assert conn.writer is writer\n        assert conn.client_addr == ('127.0.0.1', 12345)\n        assert conn.streams == {}\n        assert conn.is_closed is False\n        assert conn._initialized is False\n\n    def test_settings_from_config(self):\n        from gunicorn.http2.async_connection import AsyncHTTP2Connection\n\n        cfg = MockConfig()\n        cfg.http2_max_concurrent_streams = 50\n\n        reader = MockAsyncReader()\n        writer = MockAsyncWriter()\n        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))\n\n        assert conn.max_concurrent_streams == 50\n\n\nclass TestAsyncHTTP2ConnectionInitiate:\n    \"\"\"Test async connection initiation.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_initiate_connection(self):\n        from gunicorn.http2.async_connection import AsyncHTTP2Connection\n\n        cfg = MockConfig()\n        reader = MockAsyncReader()\n        writer = MockAsyncWriter()\n        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))\n\n        await conn.initiate_connection()\n\n        assert conn._initialized is True\n        written_data = writer.get_written_data()\n        assert len(written_data) > 0\n\n    @pytest.mark.asyncio\n    async def test_initiate_connection_idempotent(self):\n        from gunicorn.http2.async_connection import AsyncHTTP2Connection\n\n        cfg = MockConfig()\n        reader = MockAsyncReader()\n        writer = MockAsyncWriter()\n        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))\n\n        await conn.initiate_connection()\n        first_len = len(writer.get_written_data())\n\n        await conn.initiate_connection()\n        second_len = len(writer.get_written_data())\n\n        assert first_len == second_len\n\n\nclass TestAsyncHTTP2ConnectionReceiveData:\n    \"\"\"Test async receiving and processing data.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_receive_empty_data_closes_connection(self):\n        from gunicorn.http2.async_connection import AsyncHTTP2Connection\n\n        cfg = MockConfig()\n        reader = MockAsyncReader()\n        reader.set_eof()\n        writer = MockAsyncWriter()\n        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))\n        await conn.initiate_connection()\n\n        requests = await conn.receive_data()\n\n        assert conn.is_closed is True\n        assert requests == []\n\n    @pytest.mark.asyncio\n    async def test_receive_simple_get_request(self):\n        from gunicorn.http2.async_connection import AsyncHTTP2Connection\n\n        cfg = MockConfig()\n        reader = MockAsyncReader()\n        writer = MockAsyncWriter()\n        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))\n        await conn.initiate_connection()\n\n        # Create client and exchange settings\n        client = create_client_connection()\n        client_preface = client.data_to_send()\n        reader.set_data(client_preface)\n\n        await conn.receive_data()\n\n        server_data = writer.get_written_data()\n        if server_data:\n            client.receive_data(server_data)\n\n        # Client sends GET request\n        client.send_headers(\n            stream_id=1,\n            headers=[\n                (':method', 'GET'),\n                (':path', '/async-test'),\n                (':scheme', 'https'),\n                (':authority', 'localhost'),\n            ],\n            end_stream=True\n        )\n        reader.set_data(client.data_to_send())\n\n        requests = await conn.receive_data()\n\n        assert len(requests) == 1\n        assert requests[0].method == 'GET'\n        assert requests[0].path == '/async-test'\n\n    @pytest.mark.asyncio\n    async def test_receive_with_timeout(self):\n        from gunicorn.http2.async_connection import AsyncHTTP2Connection\n\n        cfg = MockConfig()\n        reader = MockAsyncReader()\n        writer = MockAsyncWriter()\n        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))\n        await conn.initiate_connection()\n\n        client = create_client_connection()\n        reader.set_data(client.data_to_send())\n\n        # Should complete without timeout\n        await conn.receive_data(timeout=5.0)\n\n    @pytest.mark.asyncio\n    async def test_receive_timeout_raises(self):\n        from gunicorn.http2.async_connection import AsyncHTTP2Connection\n\n        cfg = MockConfig()\n\n        # Create a reader that blocks forever\n        async def blocking_read(n):\n            await asyncio.sleep(10)\n            return b''\n\n        reader = mock.Mock()\n        reader.read = mock.AsyncMock(side_effect=blocking_read)\n        writer = MockAsyncWriter()\n\n        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))\n        await conn.initiate_connection()\n\n        # Timeout is converted to HTTP2ConnectionError by the implementation\n        with pytest.raises((asyncio.TimeoutError, HTTP2ConnectionError)):\n            await conn.receive_data(timeout=0.01)\n\n\nclass TestAsyncHTTP2ConnectionSendResponse:\n    \"\"\"Test async sending responses.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_send_simple_response(self):\n        from gunicorn.http2.async_connection import AsyncHTTP2Connection\n\n        cfg = MockConfig()\n        reader = MockAsyncReader()\n        writer = MockAsyncWriter()\n        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))\n        await conn.initiate_connection()\n\n        # Setup stream via request\n        client = create_client_connection()\n        reader.set_data(client.data_to_send())\n        await conn.receive_data()\n\n        client.receive_data(writer.get_written_data())\n\n        client.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'localhost'),\n        ], end_stream=True)\n        reader.set_data(client.data_to_send())\n        await conn.receive_data()\n\n        writer.clear()\n        await conn.send_response(\n            stream_id=1,\n            status=200,\n            headers=[('content-type', 'text/plain')],\n            body=b'Async Hello!'\n        )\n\n        events = client.receive_data(writer.get_written_data())\n        data_events = [e for e in events if isinstance(e, h2.events.DataReceived)]\n        assert len(data_events) == 1\n        assert data_events[0].data == b'Async Hello!'\n\n    @pytest.mark.asyncio\n    async def test_send_response_invalid_stream(self):\n        \"\"\"Test that sending response on invalid stream returns False.\"\"\"\n        from gunicorn.http2.async_connection import AsyncHTTP2Connection\n\n        cfg = MockConfig()\n        reader = MockAsyncReader()\n        writer = MockAsyncWriter()\n        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))\n        await conn.initiate_connection()\n\n        # Sending to a non-existent stream should return False gracefully\n        result = await conn.send_response(stream_id=999, status=200, headers=[], body=None)\n        assert result is False\n\n\nclass TestAsyncHTTP2ConnectionSendData:\n    \"\"\"Test async send_data method.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_send_data(self):\n        from gunicorn.http2.async_connection import AsyncHTTP2Connection\n\n        cfg = MockConfig()\n        reader = MockAsyncReader()\n        writer = MockAsyncWriter()\n        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))\n        await conn.initiate_connection()\n\n        # Setup stream\n        client = create_client_connection()\n        reader.set_data(client.data_to_send())\n        await conn.receive_data()\n        client.receive_data(writer.get_written_data())\n\n        client.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'localhost'),\n        ], end_stream=True)\n        reader.set_data(client.data_to_send())\n        await conn.receive_data()\n\n        # Send full response using send_response\n        writer.clear()\n        await conn.send_response(\n            stream_id=1,\n            status=200,\n            headers=[('content-type', 'text/plain')],\n            body=b'chunk1chunk2'\n        )\n\n        events = client.receive_data(writer.get_written_data())\n        data_events = [e for e in events if isinstance(e, h2.events.DataReceived)]\n        assert len(data_events) >= 1\n        all_data = b''.join(e.data for e in data_events)\n        assert all_data == b'chunk1chunk2'\n\n\ndef get_h2_header_value(headers_list, name):\n    \"\"\"Extract a header value from h2 headers list.\"\"\"\n    for header_name, header_value in headers_list:\n        name_str = header_name.decode() if isinstance(header_name, bytes) else header_name\n        if name_str == name:\n            return header_value.decode() if isinstance(header_value, bytes) else header_value\n    return None\n\n\nclass TestAsyncHTTP2ConnectionSendError:\n    \"\"\"Test async error response sending.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_send_error(self):\n        from gunicorn.http2.async_connection import AsyncHTTP2Connection\n\n        cfg = MockConfig()\n        reader = MockAsyncReader()\n        writer = MockAsyncWriter()\n        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))\n        await conn.initiate_connection()\n\n        client = create_client_connection()\n        reader.set_data(client.data_to_send())\n        await conn.receive_data()\n        client.receive_data(writer.get_written_data())\n\n        client.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'localhost'),\n        ], end_stream=True)\n        reader.set_data(client.data_to_send())\n        await conn.receive_data()\n\n        writer.clear()\n        await conn.send_error(stream_id=1, status_code=500, message=\"Internal Error\")\n\n        events = client.receive_data(writer.get_written_data())\n        response_events = [e for e in events if isinstance(e, h2.events.ResponseReceived)]\n        assert len(response_events) == 1\n        headers_list = response_events[0].headers\n        assert get_h2_header_value(headers_list, ':status') == '500'\n\n\nclass TestAsyncHTTP2ConnectionResetStream:\n    \"\"\"Test async stream reset.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_reset_stream(self):\n        from gunicorn.http2.async_connection import AsyncHTTP2Connection\n\n        cfg = MockConfig()\n        reader = MockAsyncReader()\n        writer = MockAsyncWriter()\n        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))\n        await conn.initiate_connection()\n\n        client = create_client_connection()\n        reader.set_data(client.data_to_send())\n        await conn.receive_data()\n        client.receive_data(writer.get_written_data())\n\n        client.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'localhost'),\n        ], end_stream=False)\n        reader.set_data(client.data_to_send())\n        await conn.receive_data()\n\n        writer.clear()\n        await conn.reset_stream(stream_id=1, error_code=0x8)\n\n        events = client.receive_data(writer.get_written_data())\n        reset_events = [e for e in events if isinstance(e, h2.events.StreamReset)]\n        assert len(reset_events) == 1\n\n\nclass TestAsyncHTTP2ConnectionClose:\n    \"\"\"Test async connection close.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_close_connection(self):\n        from gunicorn.http2.async_connection import AsyncHTTP2Connection\n\n        cfg = MockConfig()\n        reader = MockAsyncReader()\n        writer = MockAsyncWriter()\n        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))\n        await conn.initiate_connection()\n\n        client = create_client_connection()\n        reader.set_data(client.data_to_send())\n        await conn.receive_data()\n\n        writer.clear()\n        await conn.close()\n\n        assert conn.is_closed is True\n        assert writer._closed is True\n\n    @pytest.mark.asyncio\n    async def test_close_idempotent(self):\n        from gunicorn.http2.async_connection import AsyncHTTP2Connection\n\n        cfg = MockConfig()\n        reader = MockAsyncReader()\n        writer = MockAsyncWriter()\n        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))\n        await conn.initiate_connection()\n\n        await conn.close()\n        await conn.close()  # Should not raise\n\n\nclass TestAsyncHTTP2ConnectionCleanup:\n    \"\"\"Test async stream cleanup.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_cleanup_stream(self):\n        from gunicorn.http2.async_connection import AsyncHTTP2Connection\n\n        cfg = MockConfig()\n        reader = MockAsyncReader()\n        writer = MockAsyncWriter()\n        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))\n        await conn.initiate_connection()\n\n        client = create_client_connection()\n        reader.set_data(client.data_to_send())\n        await conn.receive_data()\n        client.receive_data(writer.get_written_data())\n\n        client.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'localhost'),\n        ], end_stream=True)\n        reader.set_data(client.data_to_send())\n        await conn.receive_data()\n\n        assert 1 in conn.streams\n        conn.cleanup_stream(1)\n        assert 1 not in conn.streams\n\n\nclass TestAsyncHTTP2ConnectionRepr:\n    \"\"\"Test async connection representation.\"\"\"\n\n    def test_repr(self):\n        from gunicorn.http2.async_connection import AsyncHTTP2Connection\n\n        cfg = MockConfig()\n        reader = MockAsyncReader()\n        writer = MockAsyncWriter()\n        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))\n\n        repr_str = repr(conn)\n        assert \"AsyncHTTP2Connection\" in repr_str\n        assert \"streams=\" in repr_str\n\n\nclass TestAsyncHTTP2ConnectionSocketErrors:\n    \"\"\"Test socket error handling in async connection.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_read_error_raises_connection_error(self):\n        from gunicorn.http2.async_connection import AsyncHTTP2Connection\n\n        cfg = MockConfig()\n        reader = mock.Mock()\n        reader.read = mock.AsyncMock(side_effect=OSError(\"Connection reset\"))\n        writer = MockAsyncWriter()\n\n        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))\n        await conn.initiate_connection()\n\n        with pytest.raises(HTTP2ConnectionError):\n            await conn.receive_data()\n\n    @pytest.mark.asyncio\n    async def test_write_error_raises_connection_error(self):\n        from gunicorn.http2.async_connection import AsyncHTTP2Connection\n\n        cfg = MockConfig()\n        reader = MockAsyncReader()\n        writer = mock.Mock()\n        writer.write = mock.Mock(side_effect=OSError(\"Broken pipe\"))\n        writer.drain = mock.AsyncMock()\n\n        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))\n\n        with pytest.raises(HTTP2ConnectionError):\n            await conn.initiate_connection()\n\n\nclass TestAsyncHTTP2ConnectionPriority:\n    \"\"\"Test async HTTP/2 priority handling.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_handle_priority_updated_existing_stream(self):\n        \"\"\"Test handling priority update for existing stream.\"\"\"\n        from gunicorn.http2.async_connection import AsyncHTTP2Connection\n\n        cfg = MockConfig()\n        reader = MockAsyncReader()\n        writer = MockAsyncWriter()\n        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))\n\n        # Create a client connection to generate frames\n        client_conn = create_client_connection()\n        client_data = client_conn.data_to_send()\n\n        # Set up reader with client preface\n        reader.set_data(client_data)\n\n        await conn.initiate_connection()\n        await conn.receive_data()\n        writer.clear()\n\n        # Send a request to create a stream\n        client_conn.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'localhost'),\n        ])\n        request_data = client_conn.data_to_send()\n        reader.set_data(request_data)\n        await conn.receive_data()\n\n        # Verify stream was created\n        assert 1 in conn.streams\n        stream = conn.streams[1]\n\n        # Default priority values\n        assert stream.priority_weight == 16\n        assert stream.priority_depends_on == 0\n\n        # Send a PRIORITY frame\n        client_conn.prioritize(1, weight=128, depends_on=0, exclusive=False)\n        priority_data = client_conn.data_to_send()\n        reader.set_data(priority_data)\n        await conn.receive_data()\n\n        # Verify priority was updated\n        assert stream.priority_weight == 128\n\n    @pytest.mark.asyncio\n    async def test_handle_priority_updated_nonexistent_stream(self):\n        \"\"\"Test that priority update for nonexistent stream is ignored.\"\"\"\n        from gunicorn.http2.async_connection import AsyncHTTP2Connection\n\n        cfg = MockConfig()\n        reader = MockAsyncReader()\n        writer = MockAsyncWriter()\n        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))\n\n        # Create a client connection\n        client_conn = create_client_connection()\n        client_data = client_conn.data_to_send()\n\n        reader.set_data(client_data)\n        await conn.initiate_connection()\n        await conn.receive_data()\n\n        # Send a PRIORITY frame for a stream that doesn't exist\n        client_conn.prioritize(99, weight=64, depends_on=0, exclusive=False)\n        priority_data = client_conn.data_to_send()\n        reader.set_data(priority_data)\n\n        # Should not raise\n        await conn.receive_data()\n\n\nclass TestAsyncHTTP2ConnectionTrailers:\n    \"\"\"Test async HTTP/2 response trailer support.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_send_trailers_after_headers_and_body(self):\n        \"\"\"Test sending trailers after response headers and body.\"\"\"\n        from gunicorn.http2.async_connection import AsyncHTTP2Connection\n\n        cfg = MockConfig()\n        reader = MockAsyncReader()\n        writer = MockAsyncWriter()\n        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))\n\n        # Create a client connection\n        client_conn = create_client_connection()\n        client_data = client_conn.data_to_send()\n        reader.set_data(client_data)\n\n        await conn.initiate_connection()\n        await conn.receive_data()\n        writer.clear()\n\n        # Send a request\n        client_conn.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'localhost'),\n        ], end_stream=True)\n        reader.set_data(client_conn.data_to_send())\n        await conn.receive_data()\n\n        # Manually send headers without ending stream (for trailer support)\n        stream = conn.streams[1]\n        response_headers = [(':status', '200'), ('content-type', 'text/plain')]\n        conn.h2_conn.send_headers(1, response_headers, end_stream=False)\n        stream.send_headers(response_headers, end_stream=False)\n        await conn._send_pending_data()\n\n        # Send body without ending stream\n        conn.h2_conn.send_data(1, b'Hello World', end_stream=False)\n        stream.send_data(b'Hello World', end_stream=False)\n        await conn._send_pending_data()\n\n        # Send trailers\n        trailers = [('grpc-status', '0'), ('grpc-message', 'OK')]\n        await conn.send_trailers(1, trailers)\n\n        # Verify stream is closed\n        assert stream.response_complete is True\n        assert stream.response_trailers == [('grpc-status', '0'), ('grpc-message', 'OK')]\n\n    @pytest.mark.asyncio\n    async def test_send_trailers_pseudo_header_raises(self):\n        \"\"\"Test that pseudo-headers in trailers raise error.\"\"\"\n        from gunicorn.http2.async_connection import AsyncHTTP2Connection\n\n        cfg = MockConfig()\n        reader = MockAsyncReader()\n        writer = MockAsyncWriter()\n        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))\n\n        client_conn = create_client_connection()\n        reader.set_data(client_conn.data_to_send())\n        await conn.initiate_connection()\n        await conn.receive_data()\n\n        # Send a request\n        client_conn.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'localhost'),\n        ], end_stream=True)\n        reader.set_data(client_conn.data_to_send())\n        await conn.receive_data()\n\n        # Send response\n        await conn.send_response(1, 200, [('content-type', 'text/plain')], None)\n\n        # Try to send trailers with pseudo-header\n        with pytest.raises(HTTP2Error) as exc_info:\n            await conn.send_trailers(1, [(':status', '200')])\n        assert \"Pseudo-header\" in str(exc_info.value)\n\n    @pytest.mark.asyncio\n    async def test_send_trailers_without_headers_returns_false(self):\n        \"\"\"Test that sending trailers without headers returns False.\"\"\"\n        from gunicorn.http2.async_connection import AsyncHTTP2Connection\n\n        cfg = MockConfig()\n        reader = MockAsyncReader()\n        writer = MockAsyncWriter()\n        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))\n\n        client_conn = create_client_connection()\n        reader.set_data(client_conn.data_to_send())\n        await conn.initiate_connection()\n        await conn.receive_data()\n\n        # Send a request\n        client_conn.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'localhost'),\n        ], end_stream=True)\n        reader.set_data(client_conn.data_to_send())\n        await conn.receive_data()\n\n        # Try to send trailers without sending headers first - should return False\n        result = await conn.send_trailers(1, [('trailer', 'value')])\n        assert result is False\n\n\nclass TestAsyncHTTP2FlowControl:\n    \"\"\"Test async HTTP/2 flow control handling.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_send_data_respects_zero_window(self):\n        \"\"\"Test that send_data returns False when flow control window is 0.\"\"\"\n        from gunicorn.http2.async_connection import AsyncHTTP2Connection\n\n        cfg = MockConfig()\n        reader = MockAsyncReader()\n        writer = MockAsyncWriter()\n        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))\n\n        # Create client and send preface\n        client_conn = create_client_connection()\n        reader.set_data(client_conn.data_to_send())\n        await conn.initiate_connection()\n        await conn.receive_data()\n\n        # Send a request\n        client_conn.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'localhost'),\n        ], end_stream=True)\n        reader.set_data(client_conn.data_to_send())\n        await conn.receive_data()\n\n        # Send response headers without ending stream\n        conn.h2_conn.send_headers(1, [\n            (':status', '200'),\n            ('content-type', 'text/plain'),\n        ], end_stream=False)\n        await conn._send_pending_data()\n        conn.streams[1].send_headers([(':status', '200')], end_stream=False)\n\n        # Mock the flow control window to return 0\n        original_window = conn.h2_conn.local_flow_control_window\n        conn.h2_conn.local_flow_control_window = lambda stream_id: 0\n\n        # Try to send data - should return False (not raise)\n        result = await conn.send_data(1, b'Hello, World!')\n        assert result is False\n\n        # Restore\n        conn.h2_conn.local_flow_control_window = original_window\n\n    @pytest.mark.asyncio\n    async def test_send_data_respects_flow_control(self):\n        \"\"\"Test that send_data chunks data according to flow control window.\"\"\"\n        from gunicorn.http2.async_connection import AsyncHTTP2Connection\n\n        cfg = MockConfig()\n        reader = MockAsyncReader()\n        writer = MockAsyncWriter()\n        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))\n\n        # Create client and send preface\n        client_conn = create_client_connection()\n        reader.set_data(client_conn.data_to_send())\n        await conn.initiate_connection()\n        await conn.receive_data()\n\n        # Send a request\n        client_conn.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'localhost'),\n        ], end_stream=True)\n        reader.set_data(client_conn.data_to_send())\n        await conn.receive_data()\n\n        # Send response headers without ending stream\n        conn.h2_conn.send_headers(1, [\n            (':status', '200'),\n            ('content-type', 'text/plain'),\n        ], end_stream=False)\n        await conn._send_pending_data()\n        conn.streams[1].send_headers([(':status', '200')], end_stream=False)\n\n        # Send small data - should succeed within window\n        small_data = b'Hello'\n        await conn.send_data(1, small_data, end_stream=True)\n\n        # Verify data was sent\n        sent_data = writer.get_written_data()\n        assert len(sent_data) > 0\n\n\nclass TestAsyncHTTP2StreamClosedHandling:\n    \"\"\"Test graceful handling of StreamClosedError in async connection.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_send_response_on_closed_stream(self):\n        \"\"\"Test that send_response gracefully handles closed stream.\"\"\"\n        from gunicorn.http2.async_connection import AsyncHTTP2Connection\n\n        cfg = MockConfig()\n        reader = MockAsyncReader()\n        writer = MockAsyncWriter()\n        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))\n\n        # Create client and send preface\n        client_conn = create_client_connection()\n        reader.set_data(client_conn.data_to_send())\n        await conn.initiate_connection()\n        await conn.receive_data()\n\n        # Send a request\n        client_conn.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'localhost'),\n        ], end_stream=True)\n        reader.set_data(client_conn.data_to_send())\n        await conn.receive_data()\n\n        # Simulate client resetting the stream\n        client_conn.reset_stream(1)\n        reader.set_data(client_conn.data_to_send())\n        await conn.receive_data()\n\n        # Try to send response - should return False, not raise\n        result = await conn.send_response(1, 200, [('content-type', 'text/plain')], b'Hello')\n        assert result is False\n\n    @pytest.mark.asyncio\n    async def test_send_data_on_reset_stream(self):\n        \"\"\"Test that send_data gracefully handles reset stream.\"\"\"\n        from gunicorn.http2.async_connection import AsyncHTTP2Connection\n\n        cfg = MockConfig()\n        reader = MockAsyncReader()\n        writer = MockAsyncWriter()\n        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))\n\n        # Create client and send preface\n        client_conn = create_client_connection()\n        reader.set_data(client_conn.data_to_send())\n        await conn.initiate_connection()\n        await conn.receive_data()\n\n        # Send a request\n        client_conn.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'localhost'),\n        ], end_stream=True)\n        reader.set_data(client_conn.data_to_send())\n        await conn.receive_data()\n\n        # Send response headers without ending stream\n        conn.h2_conn.send_headers(1, [\n            (':status', '200'),\n            ('content-type', 'text/plain'),\n        ], end_stream=False)\n        await conn._send_pending_data()\n        conn.streams[1].send_headers([(':status', '200')], end_stream=False)\n\n        # Simulate client resetting the stream\n        client_conn.reset_stream(1)\n        reader.set_data(client_conn.data_to_send())\n        await conn.receive_data()\n\n        # Try to send data - should return False, not raise\n        result = await conn.send_data(1, b'Hello, World!', end_stream=True)\n        assert result is False\n\n\nclass TestAsyncHTTP2WindowOverflowHandling:\n    \"\"\"Test window overflow handling in async connection.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_window_overflow_sends_goaway(self):\n        \"\"\"Test that window overflow results in connection close.\"\"\"\n        from gunicorn.http2.async_connection import AsyncHTTP2Connection\n        from gunicorn.http2.errors import HTTP2ErrorCode\n\n        cfg = MockConfig()\n        reader = MockAsyncReader()\n        writer = MockAsyncWriter()\n        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))\n\n        # Create client and send preface\n        client_conn = create_client_connection()\n        reader.set_data(client_conn.data_to_send())\n        await conn.initiate_connection()\n        await conn.receive_data()\n\n        # Mock increment_flow_control_window to raise ValueError (overflow)\n        def raise_overflow(increment, stream_id=None):\n            raise ValueError(\"Flow control window too large\")\n\n        conn.h2_conn.increment_flow_control_window = raise_overflow\n\n        # Send a request with data to trigger the overflow\n        client_conn.send_headers(1, [\n            (':method', 'POST'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'localhost'),\n        ], end_stream=False)\n        client_conn.send_data(1, b'test data', end_stream=True)\n        reader.set_data(client_conn.data_to_send())\n        await conn.receive_data()\n\n        # Connection should be closed with FLOW_CONTROL_ERROR\n        assert conn.is_closed is True\n\n\nclass TestAsyncHTTP2ProtocolErrorHandling:\n    \"\"\"Test protocol error handling sends proper GOAWAY.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_protocol_error_sends_goaway(self):\n        \"\"\"Test that protocol errors result in GOAWAY being sent.\"\"\"\n        from gunicorn.http2.async_connection import AsyncHTTP2Connection\n        from gunicorn.http2.errors import HTTP2ProtocolError, HTTP2ErrorCode\n\n        cfg = MockConfig()\n        reader = MockAsyncReader()\n        writer = MockAsyncWriter()\n        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))\n\n        # Create client and send preface\n        client_conn = create_client_connection()\n        reader.set_data(client_conn.data_to_send())\n        await conn.initiate_connection()\n        await conn.receive_data()\n\n        # Clear sent data to only capture new frames\n        writer.clear()\n\n        # Mock h2_conn.receive_data to raise ProtocolError\n        def raise_protocol_error(data):\n            raise h2.exceptions.ProtocolError(\"Test protocol error\")\n\n        conn.h2_conn.receive_data = raise_protocol_error\n\n        # Set some dummy data for the reader\n        reader.set_data(b'dummy data')\n\n        # This should send GOAWAY and raise ProtocolError\n        with pytest.raises(HTTP2ProtocolError) as exc_info:\n            await conn.receive_data()\n\n        assert \"Test protocol error\" in str(exc_info.value)\n\n        # Verify something was sent (GOAWAY frame)\n        sent_data = writer.get_written_data()\n        assert len(sent_data) > 0\n        # Connection should be marked as closed\n        assert conn.is_closed is True\n"
  },
  {
    "path": "tests/test_http2_config.py",
    "content": "# -*- coding: utf-8 -\n#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Tests for HTTP/2 configuration settings.\"\"\"\n\nimport pytest\n\nfrom gunicorn import config\nfrom gunicorn.config import Config\n\n\nclass TestHttpProtocolsConfig:\n    \"\"\"Test http_protocols configuration setting.\"\"\"\n\n    def test_default_is_h1(self):\n        c = Config()\n        assert c.http_protocols == [\"h1\"]\n\n    def test_set_h1_only(self):\n        c = Config()\n        c.set(\"http_protocols\", \"h1\")\n        assert c.http_protocols == [\"h1\"]\n\n    def test_set_h2_only(self):\n        c = Config()\n        c.set(\"http_protocols\", \"h2\")\n        assert c.http_protocols == [\"h2\"]\n\n    def test_set_h1_and_h2(self):\n        c = Config()\n        c.set(\"http_protocols\", \"h2,h1\")\n        assert c.http_protocols == [\"h2\", \"h1\"]\n\n    def test_set_h1_h2_order_preserved(self):\n        c = Config()\n        c.set(\"http_protocols\", \"h1,h2\")\n        assert c.http_protocols == [\"h1\", \"h2\"]\n\n    def test_whitespace_handling(self):\n        c = Config()\n        c.set(\"http_protocols\", \" h1 , h2 \")\n        assert c.http_protocols == [\"h1\", \"h2\"]\n\n    def test_case_insensitive(self):\n        c = Config()\n        c.set(\"http_protocols\", \"H1,H2\")\n        assert c.http_protocols == [\"h1\", \"h2\"]\n\n    def test_empty_string_defaults_to_h1(self):\n        c = Config()\n        c.set(\"http_protocols\", \"\")\n        assert c.http_protocols == [\"h1\"]\n\n    def test_none_defaults_to_h1(self):\n        c = Config()\n        c.set(\"http_protocols\", None)\n        assert c.http_protocols == [\"h1\"]\n\n    def test_invalid_protocol(self):\n        c = Config()\n        with pytest.raises(ValueError) as exc_info:\n            c.set(\"http_protocols\", \"h4\")\n        assert \"Invalid protocol\" in str(exc_info.value)\n        assert \"h4\" in str(exc_info.value)\n\n    def test_invalid_type(self):\n        c = Config()\n        with pytest.raises(TypeError) as exc_info:\n            c.set(\"http_protocols\", 123)\n        assert \"must be a string\" in str(exc_info.value)\n\n    def test_invalid_type_list(self):\n        c = Config()\n        with pytest.raises(TypeError):\n            c.set(\"http_protocols\", [\"h1\", \"h2\"])\n\n    def test_mixed_valid_invalid(self):\n        c = Config()\n        with pytest.raises(ValueError):\n            c.set(\"http_protocols\", \"h1,invalid,h2\")\n\n\nclass TestHttp2MaxConcurrentStreams:\n    \"\"\"Test http2_max_concurrent_streams configuration setting.\"\"\"\n\n    def test_default_value(self):\n        c = Config()\n        assert c.http2_max_concurrent_streams == 100\n\n    def test_set_custom_value(self):\n        c = Config()\n        c.set(\"http2_max_concurrent_streams\", 50)\n        assert c.http2_max_concurrent_streams == 50\n\n    def test_set_from_string(self):\n        c = Config()\n        c.set(\"http2_max_concurrent_streams\", \"200\")\n        assert c.http2_max_concurrent_streams == 200\n\n    def test_set_high_value(self):\n        c = Config()\n        c.set(\"http2_max_concurrent_streams\", 1000)\n        assert c.http2_max_concurrent_streams == 1000\n\n    def test_negative_value_raises(self):\n        c = Config()\n        with pytest.raises(ValueError):\n            c.set(\"http2_max_concurrent_streams\", -1)\n\n    def test_zero_value(self):\n        # Zero is technically valid for positive int validator\n        # It may have special meaning (use h2 default)\n        c = Config()\n        c.set(\"http2_max_concurrent_streams\", 0)\n        assert c.http2_max_concurrent_streams == 0\n\n\nclass TestHttp2InitialWindowSize:\n    \"\"\"Test http2_initial_window_size configuration setting.\"\"\"\n\n    def test_default_value(self):\n        c = Config()\n        # Default per RFC 7540 is 65535\n        assert c.http2_initial_window_size == 65535\n\n    def test_set_custom_value(self):\n        c = Config()\n        c.set(\"http2_initial_window_size\", 131072)\n        assert c.http2_initial_window_size == 131072\n\n    def test_set_from_string(self):\n        c = Config()\n        c.set(\"http2_initial_window_size\", \"32768\")\n        assert c.http2_initial_window_size == 32768\n\n    def test_negative_value_raises(self):\n        c = Config()\n        with pytest.raises(ValueError):\n            c.set(\"http2_initial_window_size\", -1)\n\n\nclass TestHttp2MaxFrameSize:\n    \"\"\"Test http2_max_frame_size configuration setting.\"\"\"\n\n    def test_default_value(self):\n        c = Config()\n        # Default per RFC 7540 is 16384\n        assert c.http2_max_frame_size == 16384\n\n    def test_set_custom_value(self):\n        c = Config()\n        c.set(\"http2_max_frame_size\", 32768)\n        assert c.http2_max_frame_size == 32768\n\n    def test_set_from_string(self):\n        c = Config()\n        c.set(\"http2_max_frame_size\", \"65536\")\n        assert c.http2_max_frame_size == 65536\n\n    def test_valid_min_value(self):\n        \"\"\"RFC 7540 minimum is 16384 (2^14).\"\"\"\n        c = Config()\n        c.set(\"http2_max_frame_size\", 16384)\n        assert c.http2_max_frame_size == 16384\n\n    def test_valid_max_value(self):\n        \"\"\"RFC 7540 maximum is 16777215 (2^24 - 1).\"\"\"\n        c = Config()\n        c.set(\"http2_max_frame_size\", 16777215)\n        assert c.http2_max_frame_size == 16777215\n\n    def test_valid_mid_range_value(self):\n        \"\"\"Test a value in the middle of the valid range.\"\"\"\n        c = Config()\n        c.set(\"http2_max_frame_size\", 1048576)  # 1MB\n        assert c.http2_max_frame_size == 1048576\n\n    def test_below_min_raises(self):\n        \"\"\"Values below 16384 should raise ValueError per RFC 7540.\"\"\"\n        c = Config()\n        with pytest.raises(ValueError) as exc_info:\n            c.set(\"http2_max_frame_size\", 16383)\n        assert \"must be between 16384 and 16777215\" in str(exc_info.value)\n\n    def test_above_max_raises(self):\n        \"\"\"Values above 16777215 should raise ValueError per RFC 7540.\"\"\"\n        c = Config()\n        with pytest.raises(ValueError) as exc_info:\n            c.set(\"http2_max_frame_size\", 16777216)\n        assert \"must be between 16384 and 16777215\" in str(exc_info.value)\n\n    def test_negative_value_raises(self):\n        c = Config()\n        with pytest.raises(ValueError):\n            c.set(\"http2_max_frame_size\", -1)\n\n\nclass TestHttp2MaxHeaderListSize:\n    \"\"\"Test http2_max_header_list_size configuration setting.\"\"\"\n\n    def test_default_value(self):\n        c = Config()\n        assert c.http2_max_header_list_size == 65536\n\n    def test_set_custom_value(self):\n        c = Config()\n        c.set(\"http2_max_header_list_size\", 131072)\n        assert c.http2_max_header_list_size == 131072\n\n    def test_set_from_string(self):\n        c = Config()\n        c.set(\"http2_max_header_list_size\", \"262144\")\n        assert c.http2_max_header_list_size == 262144\n\n    def test_negative_value_raises(self):\n        c = Config()\n        with pytest.raises(ValueError):\n            c.set(\"http2_max_header_list_size\", -1)\n\n\nclass TestHttp2ConfigPropertyAccess:\n    \"\"\"Test property access for HTTP/2 settings.\"\"\"\n\n    def test_all_http2_settings_accessible(self):\n        c = Config()\n        # These should not raise\n        _ = c.http_protocols\n        _ = c.http2_max_concurrent_streams\n        _ = c.http2_initial_window_size\n        _ = c.http2_max_frame_size\n        _ = c.http2_max_header_list_size\n\n\nclass TestHttp2ConfigDefaults:\n    \"\"\"Test that defaults match HTTP/2 specification values.\"\"\"\n\n    def test_window_size_matches_rfc(self):\n        \"\"\"RFC 7540 default is 2^16-1 (65535).\"\"\"\n        c = Config()\n        assert c.http2_initial_window_size == 65535\n\n    def test_max_frame_size_matches_rfc_minimum(self):\n        \"\"\"RFC 7540 minimum is 2^14 (16384).\"\"\"\n        c = Config()\n        assert c.http2_max_frame_size == 16384\n\n    def test_concurrent_streams_reasonable_default(self):\n        \"\"\"Default should be reasonable for production use.\"\"\"\n        c = Config()\n        assert 1 <= c.http2_max_concurrent_streams <= 1000\n\n\nclass TestValidateHttpProtocols:\n    \"\"\"Test the validate_http_protocols function directly.\"\"\"\n\n    def test_validate_none(self):\n        result = config.validate_http_protocols(None)\n        assert result == [\"h1\"]\n\n    def test_validate_empty_string(self):\n        result = config.validate_http_protocols(\"\")\n        assert result == [\"h1\"]\n\n    def test_validate_whitespace_only(self):\n        result = config.validate_http_protocols(\"   \")\n        assert result == [\"h1\"]\n\n    def test_validate_single_protocol(self):\n        result = config.validate_http_protocols(\"h2\")\n        assert result == [\"h2\"]\n\n    def test_validate_multiple_protocols(self):\n        result = config.validate_http_protocols(\"h2,h1\")\n        assert result == [\"h2\", \"h1\"]\n\n    def test_validate_with_spaces(self):\n        result = config.validate_http_protocols(\"h2 , h1\")\n        assert result == [\"h2\", \"h1\"]\n\n    def test_validate_uppercase(self):\n        result = config.validate_http_protocols(\"H2,H1\")\n        assert result == [\"h1\", \"h2\"] or result == [\"h2\", \"h1\"]\n\n    def test_validate_invalid_raises(self):\n        with pytest.raises(ValueError):\n            config.validate_http_protocols(\"http2\")\n\n    def test_validate_type_error(self):\n        with pytest.raises(TypeError):\n            config.validate_http_protocols(42)\n\n\nclass TestValidateHttp2FrameSize:\n    \"\"\"Test the validate_http2_frame_size function directly.\"\"\"\n\n    def test_validate_min_value(self):\n        \"\"\"RFC 7540 minimum is 16384 (2^14).\"\"\"\n        result = config.validate_http2_frame_size(16384)\n        assert result == 16384\n\n    def test_validate_max_value(self):\n        \"\"\"RFC 7540 maximum is 16777215 (2^24 - 1).\"\"\"\n        result = config.validate_http2_frame_size(16777215)\n        assert result == 16777215\n\n    def test_validate_mid_range(self):\n        \"\"\"Test a value in the middle of the valid range.\"\"\"\n        result = config.validate_http2_frame_size(1000000)\n        assert result == 1000000\n\n    def test_validate_from_string(self):\n        \"\"\"Test that string values are converted properly.\"\"\"\n        result = config.validate_http2_frame_size(\"32768\")\n        assert result == 32768\n\n    def test_validate_hex_string(self):\n        \"\"\"Test hex string conversion.\"\"\"\n        result = config.validate_http2_frame_size(\"0x10000\")  # 65536\n        assert result == 65536\n\n    def test_validate_below_min_raises(self):\n        \"\"\"Values below 16384 should raise ValueError.\"\"\"\n        with pytest.raises(ValueError) as exc_info:\n            config.validate_http2_frame_size(16383)\n        assert \"must be between 16384 and 16777215\" in str(exc_info.value)\n\n    def test_validate_above_max_raises(self):\n        \"\"\"Values above 16777215 should raise ValueError.\"\"\"\n        with pytest.raises(ValueError) as exc_info:\n            config.validate_http2_frame_size(16777216)\n        assert \"must be between 16384 and 16777215\" in str(exc_info.value)\n\n    def test_validate_zero_raises(self):\n        \"\"\"Zero is below minimum and should raise ValueError.\"\"\"\n        with pytest.raises(ValueError):\n            config.validate_http2_frame_size(0)\n\n    def test_validate_negative_raises(self):\n        \"\"\"Negative values should raise ValueError.\"\"\"\n        with pytest.raises(ValueError):\n            config.validate_http2_frame_size(-1)\n"
  },
  {
    "path": "tests/test_http2_connection.py",
    "content": "# -*- coding: utf-8 -\n#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Tests for HTTP/2 server connection.\"\"\"\n\nimport pytest\nfrom unittest import mock\nfrom io import BytesIO\n\n# Check if h2 is available for integration tests\ntry:\n    import h2.connection\n    import h2.config\n    import h2.events\n    import h2.exceptions\n    H2_AVAILABLE = True\nexcept ImportError:\n    H2_AVAILABLE = False\n\nfrom gunicorn.http2.errors import (\n    HTTP2Error, HTTP2ConnectionError\n)\n\n\npytestmark = pytest.mark.skipif(not H2_AVAILABLE, reason=\"h2 library not available\")\n\n\nclass MockConfig:\n    \"\"\"Mock gunicorn configuration for HTTP/2.\"\"\"\n\n    def __init__(self):\n        self.http2_max_concurrent_streams = 100\n        self.http2_initial_window_size = 65535\n        self.http2_max_frame_size = 16384\n        self.http2_max_header_list_size = 65536\n\n\nclass MockSocket:\n    \"\"\"Mock socket for testing connection without real network I/O.\"\"\"\n\n    def __init__(self, data=b''):\n        self._recv_buffer = BytesIO(data)\n        self._sent = bytearray()\n        self._closed = False\n\n    def recv(self, size):\n        return self._recv_buffer.read(size)\n\n    def sendall(self, data):\n        if self._closed:\n            raise OSError(\"Socket is closed\")\n        self._sent.extend(data)\n\n    def close(self):\n        self._closed = True\n\n    def get_sent_data(self):\n        return bytes(self._sent)\n\n    def set_recv_data(self, data):\n        self._recv_buffer = BytesIO(data)\n\n\ndef create_client_connection():\n    \"\"\"Create an h2 client connection for generating test frames.\"\"\"\n    config = h2.config.H2Configuration(client_side=True)\n    conn = h2.connection.H2Connection(config=config)\n    conn.initiate_connection()\n    return conn\n\n\nclass TestHTTP2ServerConnectionInit:\n    \"\"\"Test HTTP2ServerConnection initialization.\"\"\"\n\n    def test_basic_initialization(self):\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n\n        assert conn.cfg is cfg\n        assert conn.sock is sock\n        assert conn.client_addr == ('127.0.0.1', 12345)\n        assert conn.streams == {}\n        assert conn.is_closed is False\n        assert conn._initialized is False\n\n    def test_settings_from_config(self):\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        cfg.http2_max_concurrent_streams = 50\n        cfg.http2_initial_window_size = 32768\n\n        sock = MockSocket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n\n        assert conn.max_concurrent_streams == 50\n        assert conn.initial_window_size == 32768\n\n\nclass TestHTTP2ServerConnectionInitiate:\n    \"\"\"Test connection initiation.\"\"\"\n\n    def test_initiate_connection(self):\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n\n        conn.initiate_connection()\n\n        assert conn._initialized is True\n        # Should have sent settings frame\n        sent_data = sock.get_sent_data()\n        assert len(sent_data) > 0\n\n    def test_initiate_connection_idempotent(self):\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n\n        conn.initiate_connection()\n        first_sent = len(sock.get_sent_data())\n\n        conn.initiate_connection()  # Second call\n        second_sent = len(sock.get_sent_data())\n\n        # Should not send additional data\n        assert first_sent == second_sent\n\n\nclass TestHTTP2ServerConnectionReceiveData:\n    \"\"\"Test receiving and processing data.\"\"\"\n\n    def test_receive_empty_data_closes_connection(self):\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket(b'')\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        conn.initiate_connection()\n\n        requests = conn.receive_data()\n\n        assert conn.is_closed is True\n        assert requests == []\n\n    def test_receive_client_preface_and_headers(self):\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        conn.initiate_connection()\n\n        # Generate client data\n        client = create_client_connection()\n        client_preface = client.data_to_send()\n\n        # Simulate server receiving client settings\n        # Feed client preface to server\n        requests = conn.receive_data(client_preface)\n\n        # No requests yet, just settings exchange\n        assert requests == []\n\n    def test_receive_simple_get_request(self):\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        conn.initiate_connection()\n\n        # Create client and send request\n        client = create_client_connection()\n        client_preface = client.data_to_send()\n\n        # Process client preface on server\n        conn.receive_data(client_preface)\n\n        # Server may have sent settings, feed them to client\n        server_data = sock.get_sent_data()\n        if server_data:\n            client.receive_data(server_data)\n\n        # Client sends GET request\n        client.send_headers(\n            stream_id=1,\n            headers=[\n                (':method', 'GET'),\n                (':path', '/test'),\n                (':scheme', 'https'),\n                (':authority', 'localhost'),\n            ],\n            end_stream=True\n        )\n        request_data = client.data_to_send()\n\n        # Server receives request\n        requests = conn.receive_data(request_data)\n\n        assert len(requests) == 1\n        req = requests[0]\n        assert req.method == 'GET'\n        assert req.path == '/test'\n\n    def test_receive_post_with_body(self):\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        conn.initiate_connection()\n\n        # Create client\n        client = create_client_connection()\n        client_preface = client.data_to_send()\n        conn.receive_data(client_preface)\n\n        server_data = sock.get_sent_data()\n        if server_data:\n            client.receive_data(server_data)\n\n        # Client sends POST with body\n        client.send_headers(\n            stream_id=1,\n            headers=[\n                (':method', 'POST'),\n                (':path', '/submit'),\n                (':scheme', 'https'),\n                (':authority', 'localhost'),\n                ('content-type', 'application/json'),\n                ('content-length', '13'),\n            ],\n            end_stream=False\n        )\n        client.send_data(stream_id=1, data=b'{\"key\":\"val\"}', end_stream=True)\n        request_data = client.data_to_send()\n\n        requests = conn.receive_data(request_data)\n\n        assert len(requests) == 1\n        req = requests[0]\n        assert req.method == 'POST'\n        assert req.body.read() == b'{\"key\":\"val\"}'\n\n    def test_socket_error_raises_connection_error(self):\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = mock.Mock()\n        sock.recv.side_effect = OSError(\"Connection reset\")\n\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        conn.initiate_connection()\n\n        with pytest.raises(HTTP2ConnectionError):\n            conn.receive_data()\n\n\nclass TestHTTP2ServerConnectionSendResponse:\n    \"\"\"Test sending responses.\"\"\"\n\n    def test_send_simple_response(self):\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        conn.initiate_connection()\n\n        # Create a stream by receiving a request\n        client = create_client_connection()\n        client_preface = client.data_to_send()\n        conn.receive_data(client_preface)\n\n        server_data = sock.get_sent_data()\n        if server_data:\n            client.receive_data(server_data)\n\n        client.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'localhost'),\n        ], end_stream=True)\n        conn.receive_data(client.data_to_send())\n\n        # Send response\n        sock._sent.clear()\n        conn.send_response(\n            stream_id=1,\n            status=200,\n            headers=[('content-type', 'text/plain')],\n            body=b'Hello!'\n        )\n\n        sent = sock.get_sent_data()\n        assert len(sent) > 0\n\n        # Verify client receives valid response\n        events = client.receive_data(sent)\n        response_events = [e for e in events if isinstance(e, h2.events.ResponseReceived)]\n        data_events = [e for e in events if isinstance(e, h2.events.DataReceived)]\n\n        assert len(response_events) == 1\n        assert len(data_events) == 1\n        assert data_events[0].data == b'Hello!'\n\n    def test_send_response_with_empty_body(self):\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        conn.initiate_connection()\n\n        client = create_client_connection()\n        conn.receive_data(client.data_to_send())\n        client.receive_data(sock.get_sent_data())\n\n        client.send_headers(1, [\n            (':method', 'HEAD'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'localhost'),\n        ], end_stream=True)\n        conn.receive_data(client.data_to_send())\n\n        sock._sent.clear()\n        conn.send_response(stream_id=1, status=200, headers=[], body=None)\n\n        events = client.receive_data(sock.get_sent_data())\n        stream_ended = [e for e in events if isinstance(e, h2.events.StreamEnded)]\n        assert len(stream_ended) == 1\n\n    def test_send_response_invalid_stream(self):\n        \"\"\"Test that sending response on invalid stream returns False.\"\"\"\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        conn.initiate_connection()\n\n        # Sending to a non-existent stream should return False gracefully\n        result = conn.send_response(stream_id=999, status=200, headers=[], body=None)\n        assert result is False\n\n\nclass TestHTTP2ServerConnectionSendError:\n    \"\"\"Test sending error responses.\"\"\"\n\n    def test_send_error_with_message(self):\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        conn.initiate_connection()\n\n        client = create_client_connection()\n        conn.receive_data(client.data_to_send())\n        client.receive_data(sock.get_sent_data())\n\n        client.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/notfound'),\n            (':scheme', 'https'),\n            (':authority', 'localhost'),\n        ], end_stream=True)\n        conn.receive_data(client.data_to_send())\n\n        sock._sent.clear()\n        conn.send_error(stream_id=1, status_code=404, message=\"Not Found\")\n\n        events = client.receive_data(sock.get_sent_data())\n        response_events = [e for e in events if isinstance(e, h2.events.ResponseReceived)]\n        data_events = [e for e in events if isinstance(e, h2.events.DataReceived)]\n\n        assert len(response_events) == 1\n        # h2 library returns headers as list of tuples, convert to dict\n        # Note: headers may be bytes or strings depending on h2 version\n        headers_list = response_events[0].headers\n        status = None\n        for name, value in headers_list:\n            name_str = name.decode() if isinstance(name, bytes) else name\n            if name_str == ':status':\n                status = value.decode() if isinstance(value, bytes) else value\n                break\n        assert status == '404'\n\n        assert len(data_events) == 1\n        assert data_events[0].data == b\"Not Found\"\n\n\nclass TestHTTP2ServerConnectionResetStream:\n    \"\"\"Test stream reset.\"\"\"\n\n    def test_reset_stream(self):\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        conn.initiate_connection()\n\n        client = create_client_connection()\n        conn.receive_data(client.data_to_send())\n        client.receive_data(sock.get_sent_data())\n\n        client.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'localhost'),\n        ], end_stream=False)\n        conn.receive_data(client.data_to_send())\n\n        sock._sent.clear()\n        conn.reset_stream(stream_id=1, error_code=0x8)  # CANCEL\n\n        events = client.receive_data(sock.get_sent_data())\n        reset_events = [e for e in events if isinstance(e, h2.events.StreamReset)]\n        assert len(reset_events) == 1\n        assert reset_events[0].error_code == 0x8\n\n\nclass TestHTTP2ServerConnectionClose:\n    \"\"\"Test connection close.\"\"\"\n\n    def test_close_connection(self):\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        conn.initiate_connection()\n\n        client = create_client_connection()\n        conn.receive_data(client.data_to_send())\n\n        sock._sent.clear()\n        conn.close()\n\n        assert conn.is_closed is True\n\n        # Should have sent GOAWAY\n        events = client.receive_data(sock.get_sent_data())\n        goaway_events = [e for e in events if isinstance(e, h2.events.ConnectionTerminated)]\n        assert len(goaway_events) == 1\n\n    def test_close_idempotent(self):\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        conn.initiate_connection()\n\n        conn.close()\n        sent_after_first = len(sock.get_sent_data())\n\n        conn.close()  # Second call\n        sent_after_second = len(sock.get_sent_data())\n\n        # Should not send additional GOAWAY\n        assert sent_after_first == sent_after_second\n\n\nclass TestHTTP2ServerConnectionCleanup:\n    \"\"\"Test stream cleanup.\"\"\"\n\n    def test_cleanup_stream(self):\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        conn.initiate_connection()\n\n        client = create_client_connection()\n        conn.receive_data(client.data_to_send())\n        client.receive_data(sock.get_sent_data())\n\n        client.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'localhost'),\n        ], end_stream=True)\n        conn.receive_data(client.data_to_send())\n\n        assert 1 in conn.streams\n\n        conn.cleanup_stream(1)\n\n        assert 1 not in conn.streams\n\n    def test_cleanup_nonexistent_stream(self):\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        conn.initiate_connection()\n\n        # Should not raise\n        conn.cleanup_stream(999)\n\n\nclass TestHTTP2ServerConnectionMultipleStreams:\n    \"\"\"Test handling multiple concurrent streams.\"\"\"\n\n    def test_multiple_streams(self):\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        conn.initiate_connection()\n\n        client = create_client_connection()\n        conn.receive_data(client.data_to_send())\n        client.receive_data(sock.get_sent_data())\n\n        # Send multiple requests\n        client.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/one'),\n            (':scheme', 'https'),\n            (':authority', 'localhost'),\n        ], end_stream=True)\n\n        client.send_headers(3, [\n            (':method', 'GET'),\n            (':path', '/two'),\n            (':scheme', 'https'),\n            (':authority', 'localhost'),\n        ], end_stream=True)\n\n        requests = conn.receive_data(client.data_to_send())\n\n        assert len(requests) == 2\n        paths = {req.path for req in requests}\n        assert paths == {'/one', '/two'}\n\n\nclass TestHTTP2ServerConnectionRepr:\n    \"\"\"Test string representation.\"\"\"\n\n    def test_repr(self):\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n\n        repr_str = repr(conn)\n        assert \"HTTP2ServerConnection\" in repr_str\n        assert \"streams=\" in repr_str\n        assert \"closed=\" in repr_str\n\n\nclass TestHTTP2ServerConnectionPriority:\n    \"\"\"Test HTTP/2 priority handling.\"\"\"\n\n    def test_handle_priority_updated_existing_stream(self):\n        \"\"\"Test handling priority update for existing stream.\"\"\"\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        conn.initiate_connection()\n\n        # Create a client connection to generate frames\n        client_conn = create_client_connection()\n\n        # Get client preface\n        client_data = client_conn.data_to_send()\n\n        # Feed client preface to server\n        conn.receive_data(client_data)\n        sock._sent = bytearray()\n\n        # Send a request to create a stream\n        client_conn.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'localhost'),\n        ])\n        request_data = client_conn.data_to_send()\n        conn.receive_data(request_data)\n\n        # Verify stream was created\n        assert 1 in conn.streams\n        stream = conn.streams[1]\n\n        # Default priority values\n        assert stream.priority_weight == 16\n        assert stream.priority_depends_on == 0\n\n        # Send a PRIORITY frame\n        client_conn.prioritize(1, weight=128, depends_on=0, exclusive=False)\n        priority_data = client_conn.data_to_send()\n        conn.receive_data(priority_data)\n\n        # Verify priority was updated\n        assert stream.priority_weight == 128\n\n    def test_handle_priority_updated_nonexistent_stream(self):\n        \"\"\"Test that priority update for nonexistent stream is ignored.\"\"\"\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        conn.initiate_connection()\n\n        # Create a client connection\n        client_conn = create_client_connection()\n        client_data = client_conn.data_to_send()\n        conn.receive_data(client_data)\n\n        # Send a PRIORITY frame for a stream that doesn't exist\n        # This should not raise an error\n        client_conn.prioritize(99, weight=64, depends_on=0, exclusive=False)\n        priority_data = client_conn.data_to_send()\n\n        # Should not raise\n        conn.receive_data(priority_data)\n\n\nclass TestHTTP2ServerConnectionTrailers:\n    \"\"\"Test HTTP/2 response trailer support.\"\"\"\n\n    def test_send_trailers_after_headers_and_body(self):\n        \"\"\"Test sending trailers after response headers and body.\"\"\"\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        conn.initiate_connection()\n\n        # Create a client connection\n        client_conn = create_client_connection()\n        client_data = client_conn.data_to_send()\n        conn.receive_data(client_data)\n        sock._sent = bytearray()\n\n        # Send a request\n        client_conn.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'localhost'),\n        ], end_stream=True)\n        request_data = client_conn.data_to_send()\n        conn.receive_data(request_data)\n\n        # Manually send headers without ending stream (for trailer support)\n        stream = conn.streams[1]\n        response_headers = [(':status', '200'), ('content-type', 'text/plain')]\n        conn.h2_conn.send_headers(1, response_headers, end_stream=False)\n        stream.send_headers(response_headers, end_stream=False)\n        conn._send_pending_data()\n\n        # Send body without ending stream\n        conn.h2_conn.send_data(1, b'Hello World', end_stream=False)\n        stream.send_data(b'Hello World', end_stream=False)\n        conn._send_pending_data()\n\n        # Send trailers\n        trailers = [('grpc-status', '0'), ('grpc-message', 'OK')]\n        conn.send_trailers(1, trailers)\n\n        # Verify stream is closed\n        assert stream.response_complete is True\n        assert stream.response_trailers == [('grpc-status', '0'), ('grpc-message', 'OK')]\n\n    def test_send_trailers_pseudo_header_raises(self):\n        \"\"\"Test that pseudo-headers in trailers raise error.\"\"\"\n        from gunicorn.http2.connection import HTTP2ServerConnection\n        from gunicorn.http2.errors import HTTP2Error\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        conn.initiate_connection()\n\n        client_conn = create_client_connection()\n        client_data = client_conn.data_to_send()\n        conn.receive_data(client_data)\n\n        # Send a request\n        client_conn.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'localhost'),\n        ], end_stream=True)\n        conn.receive_data(client_conn.data_to_send())\n\n        # Send response\n        conn.send_response(1, 200, [('content-type', 'text/plain')], None)\n\n        # Try to send trailers with pseudo-header\n        with pytest.raises(HTTP2Error) as exc_info:\n            conn.send_trailers(1, [(':status', '200')])\n        assert \"Pseudo-header\" in str(exc_info.value)\n\n    def test_send_trailers_without_headers_returns_false(self):\n        \"\"\"Test that sending trailers without headers returns False.\"\"\"\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        conn.initiate_connection()\n\n        client_conn = create_client_connection()\n        client_data = client_conn.data_to_send()\n        conn.receive_data(client_data)\n\n        # Send a request\n        client_conn.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'localhost'),\n        ], end_stream=True)\n        conn.receive_data(client_conn.data_to_send())\n\n        # Try to send trailers without sending headers first - should return False\n        result = conn.send_trailers(1, [('trailer', 'value')])\n        assert result is False\n\n    def test_send_trailers_nonexistent_stream_returns_false(self):\n        \"\"\"Test that sending trailers on nonexistent stream returns False.\"\"\"\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        conn.initiate_connection()\n\n        client_conn = create_client_connection()\n        conn.receive_data(client_conn.data_to_send())\n\n        # Sending trailers to non-existent stream should return False\n        result = conn.send_trailers(99, [('trailer', 'value')])\n        assert result is False\n\n\nclass TestHTTP2FlowControl:\n    \"\"\"Test HTTP/2 flow control handling.\"\"\"\n\n    def test_send_data_respects_zero_window(self):\n        \"\"\"Test that send_data returns False when flow control window is 0.\"\"\"\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        conn.initiate_connection()\n\n        # Create client and send preface\n        client_conn = create_client_connection()\n        conn.receive_data(client_conn.data_to_send())\n\n        # Send a request\n        client_conn.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'localhost'),\n        ], end_stream=True)\n        conn.receive_data(client_conn.data_to_send())\n\n        # Send response headers without ending stream (pass body=b'' placeholder)\n        # We need to send headers first, so use h2_conn directly\n        conn.h2_conn.send_headers(1, [\n            (':status', '200'),\n            ('content-type', 'text/plain'),\n        ], end_stream=False)\n        conn._send_pending_data()\n        conn.streams[1].send_headers([(':status', '200')], end_stream=False)\n\n        # Mock the flow control window to return 0\n        original_window = conn.h2_conn.local_flow_control_window\n        conn.h2_conn.local_flow_control_window = lambda stream_id: 0\n\n        # Try to send data - should return False (not raise)\n        result = conn.send_data(1, b'Hello, World!')\n        assert result is False\n\n        # Restore\n        conn.h2_conn.local_flow_control_window = original_window\n\n    def test_send_data_respects_flow_control(self):\n        \"\"\"Test that send_data chunks data according to flow control window.\"\"\"\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        conn.initiate_connection()\n\n        # Create client and send preface\n        client_conn = create_client_connection()\n        conn.receive_data(client_conn.data_to_send())\n\n        # Send a request\n        client_conn.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'localhost'),\n        ], end_stream=True)\n        conn.receive_data(client_conn.data_to_send())\n\n        # Send response headers without ending stream\n        conn.h2_conn.send_headers(1, [\n            (':status', '200'),\n            ('content-type', 'text/plain'),\n        ], end_stream=False)\n        conn._send_pending_data()\n        conn.streams[1].send_headers([(':status', '200')], end_stream=False)\n\n        # Send small data - should succeed within window\n        small_data = b'Hello'\n        conn.send_data(1, small_data, end_stream=True)\n\n        # Verify data was sent\n        sent_data = sock.get_sent_data()\n        assert len(sent_data) > 0\n\n\nclass TestHTTP2StreamClosedHandling:\n    \"\"\"Test graceful handling of StreamClosedError.\"\"\"\n\n    def test_send_response_on_closed_stream(self):\n        \"\"\"Test that send_response gracefully handles closed stream.\"\"\"\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        conn.initiate_connection()\n\n        # Create client and send preface\n        client_conn = create_client_connection()\n        conn.receive_data(client_conn.data_to_send())\n\n        # Send a request\n        client_conn.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'localhost'),\n        ], end_stream=True)\n        conn.receive_data(client_conn.data_to_send())\n\n        # Simulate client resetting the stream\n        client_conn.reset_stream(1)\n        conn.receive_data(client_conn.data_to_send())\n\n        # Try to send response - should return False, not raise\n        result = conn.send_response(1, 200, [('content-type', 'text/plain')], b'Hello')\n        assert result is False\n\n    def test_send_data_on_reset_stream(self):\n        \"\"\"Test that send_data gracefully handles reset stream.\"\"\"\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        conn.initiate_connection()\n\n        # Create client and send preface\n        client_conn = create_client_connection()\n        conn.receive_data(client_conn.data_to_send())\n\n        # Send a request\n        client_conn.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'localhost'),\n        ], end_stream=True)\n        conn.receive_data(client_conn.data_to_send())\n\n        # Send response headers without ending stream\n        conn.h2_conn.send_headers(1, [\n            (':status', '200'),\n            ('content-type', 'text/plain'),\n        ], end_stream=False)\n        conn._send_pending_data()\n        conn.streams[1].send_headers([(':status', '200')], end_stream=False)\n\n        # Simulate client resetting the stream\n        client_conn.reset_stream(1)\n        conn.receive_data(client_conn.data_to_send())\n\n        # Try to send data - should return False, not raise\n        result = conn.send_data(1, b'Hello, World!', end_stream=True)\n        assert result is False\n\n\nclass TestHTTP2WindowOverflowHandling:\n    \"\"\"Test window overflow handling.\"\"\"\n\n    def test_window_overflow_sends_goaway(self):\n        \"\"\"Test that window overflow results in GOAWAY with FLOW_CONTROL_ERROR.\"\"\"\n        from gunicorn.http2.connection import HTTP2ServerConnection\n        from gunicorn.http2.errors import HTTP2ErrorCode\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        conn.initiate_connection()\n\n        # Create client and send preface\n        client_conn = create_client_connection()\n        conn.receive_data(client_conn.data_to_send())\n\n        # Mock increment_flow_control_window to raise ValueError (overflow)\n        original_increment = conn.h2_conn.increment_flow_control_window\n\n        def raise_overflow(increment, stream_id=None):\n            raise ValueError(\"Flow control window too large\")\n\n        conn.h2_conn.increment_flow_control_window = raise_overflow\n\n        # Send a request with data to trigger the overflow\n        client_conn.send_headers(1, [\n            (':method', 'POST'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'localhost'),\n        ], end_stream=False)\n        client_conn.send_data(1, b'test data', end_stream=True)\n        conn.receive_data(client_conn.data_to_send())\n\n        # Connection should be closed with FLOW_CONTROL_ERROR\n        assert conn.is_closed is True\n\n\nclass TestHTTP2ProtocolErrorHandling:\n    \"\"\"Test protocol error handling sends proper GOAWAY.\"\"\"\n\n    def test_protocol_error_sends_goaway(self):\n        \"\"\"Test that protocol errors result in GOAWAY being sent.\"\"\"\n        from gunicorn.http2.connection import HTTP2ServerConnection\n        from gunicorn.http2.errors import HTTP2ProtocolError, HTTP2ErrorCode\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        conn.initiate_connection()\n\n        # Create client and send preface\n        client_conn = create_client_connection()\n        conn.receive_data(client_conn.data_to_send())\n\n        # Clear sent data to only capture new frames\n        sock._sent.clear()\n\n        # Mock h2_conn.receive_data to raise ProtocolError\n        def raise_protocol_error(data):\n            raise h2.exceptions.ProtocolError(\"Test protocol error\")\n\n        conn.h2_conn.receive_data = raise_protocol_error\n\n        # This should send GOAWAY and raise ProtocolError\n        with pytest.raises(HTTP2ProtocolError) as exc_info:\n            conn.receive_data(b'dummy data')\n\n        assert \"Test protocol error\" in str(exc_info.value)\n\n        # Verify something was sent (GOAWAY frame)\n        sent_data = sock.get_sent_data()\n        assert len(sent_data) > 0\n        # Connection should be marked as closed\n        assert conn.is_closed is True\n\n\nclass TestHTTP2NotAvailable:\n    \"\"\"Test behavior when h2 is not available.\"\"\"\n\n    def test_import_error_raises_not_available(self):\n        from gunicorn.http2 import errors\n\n        # Test that HTTP2NotAvailable can be raised\n        with pytest.raises(errors.HTTP2NotAvailable):\n            raise errors.HTTP2NotAvailable()\n"
  },
  {
    "path": "tests/test_http2_errors.py",
    "content": "# -*- coding: utf-8 -\n#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Tests for HTTP/2 error classes.\"\"\"\n\nimport pytest\n\nfrom gunicorn.http2.errors import (\n    HTTP2Error,\n    HTTP2ProtocolError,\n    HTTP2InternalError,\n    HTTP2FlowControlError,\n    HTTP2SettingsTimeout,\n    HTTP2StreamClosed,\n    HTTP2FrameSizeError,\n    HTTP2RefusedStream,\n    HTTP2Cancel,\n    HTTP2CompressionError,\n    HTTP2ConnectError,\n    HTTP2EnhanceYourCalm,\n    HTTP2InadequateSecurity,\n    HTTP2RequiresHTTP11,\n    HTTP2StreamError,\n    HTTP2ConnectionError,\n    HTTP2ConfigurationError,\n    HTTP2NotAvailable,\n)\n\n\nclass TestHTTP2ErrorCodes:\n    \"\"\"Test RFC 7540 error codes.\"\"\"\n\n    def test_no_error(self):\n        err = HTTP2Error()\n        assert err.error_code == 0x0\n\n    def test_protocol_error(self):\n        err = HTTP2ProtocolError()\n        assert err.error_code == 0x1\n\n    def test_internal_error(self):\n        err = HTTP2InternalError()\n        assert err.error_code == 0x2\n\n    def test_flow_control_error(self):\n        err = HTTP2FlowControlError()\n        assert err.error_code == 0x3\n\n    def test_settings_timeout(self):\n        err = HTTP2SettingsTimeout()\n        assert err.error_code == 0x4\n\n    def test_stream_closed(self):\n        err = HTTP2StreamClosed()\n        assert err.error_code == 0x5\n\n    def test_frame_size_error(self):\n        err = HTTP2FrameSizeError()\n        assert err.error_code == 0x6\n\n    def test_refused_stream(self):\n        err = HTTP2RefusedStream()\n        assert err.error_code == 0x7\n\n    def test_cancel(self):\n        err = HTTP2Cancel()\n        assert err.error_code == 0x8\n\n    def test_compression_error(self):\n        err = HTTP2CompressionError()\n        assert err.error_code == 0x9\n\n    def test_connect_error(self):\n        err = HTTP2ConnectError()\n        assert err.error_code == 0xa\n\n    def test_enhance_your_calm(self):\n        err = HTTP2EnhanceYourCalm()\n        assert err.error_code == 0xb\n\n    def test_inadequate_security(self):\n        err = HTTP2InadequateSecurity()\n        assert err.error_code == 0xc\n\n    def test_http11_required(self):\n        err = HTTP2RequiresHTTP11()\n        assert err.error_code == 0xd\n\n\nclass TestHTTP2ErrorInheritance:\n    \"\"\"Test error class inheritance.\"\"\"\n\n    def test_all_inherit_from_http2error(self):\n        error_classes = [\n            HTTP2ProtocolError,\n            HTTP2InternalError,\n            HTTP2FlowControlError,\n            HTTP2SettingsTimeout,\n            HTTP2StreamClosed,\n            HTTP2FrameSizeError,\n            HTTP2RefusedStream,\n            HTTP2Cancel,\n            HTTP2CompressionError,\n            HTTP2ConnectError,\n            HTTP2EnhanceYourCalm,\n            HTTP2InadequateSecurity,\n            HTTP2RequiresHTTP11,\n            HTTP2StreamError,\n            HTTP2ConnectionError,\n            HTTP2ConfigurationError,\n            HTTP2NotAvailable,\n        ]\n        for cls in error_classes:\n            assert issubclass(cls, HTTP2Error)\n            assert issubclass(cls, Exception)\n\n    def test_http2error_is_exception(self):\n        assert issubclass(HTTP2Error, Exception)\n\n\nclass TestHTTP2ErrorMessages:\n    \"\"\"Test error message handling.\"\"\"\n\n    def test_default_message_from_docstring(self):\n        err = HTTP2ProtocolError()\n        assert err.message == \"Protocol error detected.\"\n        assert str(err) == \"Protocol error detected.\"\n\n    def test_custom_message(self):\n        err = HTTP2ProtocolError(\"Custom error message\")\n        assert err.message == \"Custom error message\"\n        assert str(err) == \"Custom error message\"\n\n    def test_custom_error_code(self):\n        err = HTTP2Error(\"Test\", error_code=0xFF)\n        assert err.error_code == 0xFF\n\n    def test_message_and_error_code(self):\n        err = HTTP2ProtocolError(\"Custom\", error_code=0x99)\n        assert err.message == \"Custom\"\n        assert err.error_code == 0x99\n\n\nclass TestHTTP2StreamError:\n    \"\"\"Test stream-specific error handling.\"\"\"\n\n    def test_stream_id_in_error(self):\n        err = HTTP2StreamError(stream_id=5)\n        assert err.stream_id == 5\n\n    def test_stream_error_str(self):\n        err = HTTP2StreamError(stream_id=7, message=\"Stream reset\")\n        assert \"Stream 7\" in str(err)\n        assert \"Stream reset\" in str(err)\n\n    def test_stream_error_default_message(self):\n        err = HTTP2StreamError(stream_id=3)\n        assert err.stream_id == 3\n        assert \"Stream 3\" in str(err)\n\n    def test_stream_error_with_error_code(self):\n        err = HTTP2StreamError(stream_id=1, error_code=0x8)\n        assert err.stream_id == 1\n        assert err.error_code == 0x8\n\n\nclass TestHTTP2ConnectionError:\n    \"\"\"Test connection-level error handling.\"\"\"\n\n    def test_connection_error_basic(self):\n        err = HTTP2ConnectionError(\"Connection failed\")\n        assert str(err) == \"Connection failed\"\n        assert isinstance(err, HTTP2Error)\n\n\nclass TestHTTP2ConfigurationError:\n    \"\"\"Test configuration error handling.\"\"\"\n\n    def test_configuration_error_basic(self):\n        err = HTTP2ConfigurationError(\"Invalid setting\")\n        assert str(err) == \"Invalid setting\"\n        assert isinstance(err, HTTP2Error)\n\n\nclass TestHTTP2NotAvailable:\n    \"\"\"Test HTTP/2 unavailable error.\"\"\"\n\n    def test_default_message(self):\n        err = HTTP2NotAvailable()\n        assert \"h2 library\" in err.message\n        assert \"pip install\" in err.message\n\n    def test_custom_message(self):\n        err = HTTP2NotAvailable(\"Custom unavailable message\")\n        assert err.message == \"Custom unavailable message\"\n\n    def test_inherits_from_http2error(self):\n        err = HTTP2NotAvailable()\n        assert isinstance(err, HTTP2Error)\n\n\nclass TestErrorRaising:\n    \"\"\"Test that errors can be properly raised and caught.\"\"\"\n\n    def test_raise_and_catch_http2error(self):\n        with pytest.raises(HTTP2Error):\n            raise HTTP2ProtocolError(\"Test\")\n\n    def test_raise_and_catch_specific(self):\n        with pytest.raises(HTTP2ProtocolError):\n            raise HTTP2ProtocolError(\"Test\")\n\n    def test_raise_stream_error(self):\n        with pytest.raises(HTTP2StreamError) as exc_info:\n            raise HTTP2StreamError(stream_id=5, message=\"Test stream error\")\n        assert exc_info.value.stream_id == 5\n\n    def test_error_chaining(self):\n        try:\n            try:\n                raise ValueError(\"Original\")\n            except ValueError as e:\n                raise HTTP2InternalError(\"Wrapped\") from e\n        except HTTP2InternalError as err:\n            assert err.__cause__ is not None\n            assert isinstance(err.__cause__, ValueError)\n"
  },
  {
    "path": "tests/test_http2_integration.py",
    "content": "# -*- coding: utf-8 -\n#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Integration tests for HTTP/2 with full request/response cycles.\"\"\"\n\nimport pytest\nfrom io import BytesIO\n\n# Check if h2 is available\ntry:\n    import h2.connection\n    import h2.config\n    import h2.events\n    H2_AVAILABLE = True\nexcept ImportError:\n    H2_AVAILABLE = False\n\n\npytestmark = pytest.mark.skipif(not H2_AVAILABLE, reason=\"h2 library not available\")\n\n\ndef get_header_value(headers_list, name):\n    \"\"\"Extract a header value from h2 headers list.\n\n    h2 library may return headers as bytes or strings depending on version.\n    \"\"\"\n    for header_name, header_value in headers_list:\n        name_str = header_name.decode() if isinstance(header_name, bytes) else header_name\n        if name_str == name:\n            return header_value.decode() if isinstance(header_value, bytes) else header_value\n    return None\n\n\nclass MockConfig:\n    \"\"\"Mock gunicorn configuration for HTTP/2.\"\"\"\n\n    def __init__(self):\n        self.http2_max_concurrent_streams = 100\n        self.http2_initial_window_size = 65535\n        self.http2_max_frame_size = 16384\n        self.http2_max_header_list_size = 65536\n\n\nclass MockSocket:\n    \"\"\"Mock socket for integration testing.\"\"\"\n\n    def __init__(self, data=b''):\n        self._recv_buffer = BytesIO(data)\n        self._sent = bytearray()\n\n    def recv(self, size):\n        return self._recv_buffer.read(size)\n\n    def sendall(self, data):\n        self._sent.extend(data)\n\n    def get_sent_data(self):\n        return bytes(self._sent)\n\n    def set_recv_data(self, data):\n        self._recv_buffer = BytesIO(data)\n\n    def clear_sent(self):\n        self._sent.clear()\n\n\ndef create_h2_client():\n    \"\"\"Create an h2 client connection.\"\"\"\n    config = h2.config.H2Configuration(client_side=True)\n    conn = h2.connection.H2Connection(config=config)\n    conn.initiate_connection()\n    return conn\n\n\nclass TestSimpleRequestResponse:\n    \"\"\"Test simple request/response cycles.\"\"\"\n\n    def test_get_request_text_response(self):\n        \"\"\"Test a complete GET request with text response.\"\"\"\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        server = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        server.initiate_connection()\n\n        # Client setup\n        client = create_h2_client()\n        sock.set_recv_data(client.data_to_send())\n        server.receive_data()\n        client.receive_data(sock.get_sent_data())\n\n        # Client sends request\n        client.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/hello'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n            ('accept', 'text/plain'),\n        ], end_stream=True)\n        sock.set_recv_data(client.data_to_send())\n\n        # Server receives request\n        requests = server.receive_data()\n        assert len(requests) == 1\n        req = requests[0]\n\n        # Verify request properties\n        assert req.method == 'GET'\n        assert req.path == '/hello'\n        assert req.version == (2, 0)\n        assert req.get_header('ACCEPT') == 'text/plain'\n\n        # Server sends response\n        sock.clear_sent()\n        server.send_response(\n            stream_id=1,\n            status=200,\n            headers=[\n                ('content-type', 'text/plain'),\n                ('content-length', '12'),\n            ],\n            body=b'Hello World!'\n        )\n\n        # Client verifies response\n        events = client.receive_data(sock.get_sent_data())\n\n        response_events = [e for e in events if isinstance(e, h2.events.ResponseReceived)]\n        assert len(response_events) == 1\n        headers_list = response_events[0].headers\n        assert get_header_value(headers_list, ':status') == '200'\n        assert get_header_value(headers_list, 'content-type') == 'text/plain'\n\n        data_events = [e for e in events if isinstance(e, h2.events.DataReceived)]\n        assert len(data_events) == 1\n        assert data_events[0].data == b'Hello World!'\n\n    def test_post_request_with_json_body(self):\n        \"\"\"Test POST request with JSON body and response.\"\"\"\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        server = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        server.initiate_connection()\n\n        client = create_h2_client()\n        sock.set_recv_data(client.data_to_send())\n        server.receive_data()\n        client.receive_data(sock.get_sent_data())\n\n        # Client sends POST with body\n        request_body = b'{\"username\": \"test\", \"action\": \"login\"}'\n        client.send_headers(1, [\n            (':method', 'POST'),\n            (':path', '/api/login'),\n            (':scheme', 'https'),\n            (':authority', 'api.example.com'),\n            ('content-type', 'application/json'),\n            ('content-length', str(len(request_body))),\n        ], end_stream=False)\n        client.send_data(1, request_body, end_stream=True)\n        sock.set_recv_data(client.data_to_send())\n\n        requests = server.receive_data()\n        assert len(requests) == 1\n        req = requests[0]\n\n        assert req.method == 'POST'\n        assert req.content_type == 'application/json'\n        assert req.body.read() == request_body\n\n        # Server responds\n        sock.clear_sent()\n        response_body = b'{\"status\": \"success\", \"token\": \"abc123\"}'\n        server.send_response(\n            stream_id=1,\n            status=200,\n            headers=[\n                ('content-type', 'application/json'),\n                ('content-length', str(len(response_body))),\n            ],\n            body=response_body\n        )\n\n        events = client.receive_data(sock.get_sent_data())\n        data_events = [e for e in events if isinstance(e, h2.events.DataReceived)]\n        assert data_events[0].data == response_body\n\n\nclass TestMultipleStreams:\n    \"\"\"Test concurrent stream handling.\"\"\"\n\n    def test_concurrent_requests(self):\n        \"\"\"Test handling multiple concurrent requests.\"\"\"\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        server = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        server.initiate_connection()\n\n        client = create_h2_client()\n        sock.set_recv_data(client.data_to_send())\n        server.receive_data()\n        client.receive_data(sock.get_sent_data())\n\n        # Client sends three concurrent requests\n        for stream_id, path in [(1, '/one'), (3, '/two'), (5, '/three')]:\n            client.send_headers(stream_id, [\n                (':method', 'GET'),\n                (':path', path),\n                (':scheme', 'https'),\n                (':authority', 'example.com'),\n            ], end_stream=True)\n\n        sock.set_recv_data(client.data_to_send())\n        requests = server.receive_data()\n\n        assert len(requests) == 3\n        paths = {req.path for req in requests}\n        assert paths == {'/one', '/two', '/three'}\n\n        # Server responds to all\n        sock.clear_sent()\n        for req in requests:\n            server.send_response(\n                stream_id=req.stream.stream_id,\n                status=200,\n                headers=[('x-path', req.path)],\n                body=req.path.encode()\n            )\n\n        events = client.receive_data(sock.get_sent_data())\n        response_events = [e for e in events if isinstance(e, h2.events.ResponseReceived)]\n        assert len(response_events) == 3\n\n    def test_interleaved_request_response(self):\n        \"\"\"Test interleaved request and response processing.\"\"\"\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        server = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        server.initiate_connection()\n\n        client = create_h2_client()\n        sock.set_recv_data(client.data_to_send())\n        server.receive_data()\n        client.receive_data(sock.get_sent_data())\n\n        # First request\n        client.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/first'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n        ], end_stream=True)\n        sock.set_recv_data(client.data_to_send())\n        requests = server.receive_data()\n        assert len(requests) == 1\n\n        # Respond to first before second arrives\n        sock.clear_sent()\n        server.send_response(1, 200, [], b'First response')\n        client.receive_data(sock.get_sent_data())\n\n        # Second request\n        client.send_headers(3, [\n            (':method', 'GET'),\n            (':path', '/second'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n        ], end_stream=True)\n        sock.set_recv_data(client.data_to_send())\n        requests = server.receive_data()\n        assert len(requests) == 1\n\n        # Respond to second\n        sock.clear_sent()\n        server.send_response(3, 200, [], b'Second response')\n        events = client.receive_data(sock.get_sent_data())\n        data_events = [e for e in events if isinstance(e, h2.events.DataReceived)]\n        assert data_events[0].data == b'Second response'\n\n\nclass TestErrorHandling:\n    \"\"\"Test error response scenarios.\"\"\"\n\n    def test_404_response(self):\n        \"\"\"Test 404 Not Found response.\"\"\"\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        server = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        server.initiate_connection()\n\n        client = create_h2_client()\n        sock.set_recv_data(client.data_to_send())\n        server.receive_data()\n        client.receive_data(sock.get_sent_data())\n\n        client.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/nonexistent'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n        ], end_stream=True)\n        sock.set_recv_data(client.data_to_send())\n        server.receive_data()\n\n        sock.clear_sent()\n        server.send_error(1, 404, \"Not Found\")\n\n        events = client.receive_data(sock.get_sent_data())\n        response_events = [e for e in events if isinstance(e, h2.events.ResponseReceived)]\n        headers_list = response_events[0].headers\n        assert get_header_value(headers_list, ':status') == '404'\n\n    def test_500_response(self):\n        \"\"\"Test 500 Internal Server Error response.\"\"\"\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        server = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        server.initiate_connection()\n\n        client = create_h2_client()\n        sock.set_recv_data(client.data_to_send())\n        server.receive_data()\n        client.receive_data(sock.get_sent_data())\n\n        client.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/error'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n        ], end_stream=True)\n        sock.set_recv_data(client.data_to_send())\n        server.receive_data()\n\n        sock.clear_sent()\n        server.send_error(1, 500, \"Internal Server Error\")\n\n        events = client.receive_data(sock.get_sent_data())\n        response_events = [e for e in events if isinstance(e, h2.events.ResponseReceived)]\n        headers_list = response_events[0].headers\n        assert get_header_value(headers_list, ':status') == '500'\n\n    def test_stream_reset_by_server(self):\n        \"\"\"Test server resetting a stream.\"\"\"\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        server = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        server.initiate_connection()\n\n        client = create_h2_client()\n        sock.set_recv_data(client.data_to_send())\n        server.receive_data()\n        client.receive_data(sock.get_sent_data())\n\n        # Start a request but don't finish\n        client.send_headers(1, [\n            (':method', 'POST'),\n            (':path', '/upload'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n        ], end_stream=False)\n        sock.set_recv_data(client.data_to_send())\n        server.receive_data()\n\n        # Server resets the stream\n        sock.clear_sent()\n        server.reset_stream(1, error_code=0x8)  # CANCEL\n\n        events = client.receive_data(sock.get_sent_data())\n        reset_events = [e for e in events if isinstance(e, h2.events.StreamReset)]\n        assert len(reset_events) == 1\n        assert reset_events[0].error_code == 0x8\n\n\nclass TestConnectionLifecycle:\n    \"\"\"Test connection lifecycle events.\"\"\"\n\n    def test_graceful_shutdown(self):\n        \"\"\"Test graceful connection shutdown with GOAWAY.\"\"\"\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        server = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        server.initiate_connection()\n\n        client = create_h2_client()\n        sock.set_recv_data(client.data_to_send())\n        server.receive_data()\n        client.receive_data(sock.get_sent_data())\n\n        # Process a request first\n        client.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n        ], end_stream=True)\n        sock.set_recv_data(client.data_to_send())\n        server.receive_data()\n\n        sock.clear_sent()\n        server.send_response(1, 200, [], b'OK')\n        client.receive_data(sock.get_sent_data())\n\n        # Server initiates graceful shutdown\n        sock.clear_sent()\n        server.close()\n\n        events = client.receive_data(sock.get_sent_data())\n        goaway_events = [e for e in events if isinstance(e, h2.events.ConnectionTerminated)]\n        assert len(goaway_events) == 1\n\n    def test_client_initiated_close(self):\n        \"\"\"Test handling client-initiated connection close.\"\"\"\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        server = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        server.initiate_connection()\n\n        client = create_h2_client()\n        sock.set_recv_data(client.data_to_send())\n        server.receive_data()\n        client.receive_data(sock.get_sent_data())\n\n        # Client closes connection\n        client.close_connection()\n        sock.set_recv_data(client.data_to_send())\n        server.receive_data()\n\n        assert server.is_closed is True\n\n\nclass TestLargePayloads:\n    \"\"\"Test handling of large payloads.\"\"\"\n\n    def test_moderate_request_body(self):\n        \"\"\"Test handling moderate-sized request body within flow control.\"\"\"\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        server = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        server.initiate_connection()\n\n        client = create_h2_client()\n        sock.set_recv_data(client.data_to_send())\n        server.receive_data()\n        client.receive_data(sock.get_sent_data())\n\n        # Send body that fits within initial window (65535 bytes)\n        body = b'X' * 10000\n        client.send_headers(1, [\n            (':method', 'POST'),\n            (':path', '/upload'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n            ('content-length', str(len(body))),\n        ], end_stream=False)\n        client.send_data(1, body, end_stream=True)\n        sock.set_recv_data(client.data_to_send())\n\n        requests = server.receive_data()\n\n        assert len(requests) == 1\n        received_body = requests[0].body.read()\n        assert len(received_body) == len(body)\n        assert received_body == body\n\n    def test_moderate_response_body(self):\n        \"\"\"Test sending moderate-sized response body.\"\"\"\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        server = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        server.initiate_connection()\n\n        client = create_h2_client()\n        sock.set_recv_data(client.data_to_send())\n        server.receive_data()\n        client.receive_data(sock.get_sent_data())\n\n        client.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/moderate'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n        ], end_stream=True)\n        sock.set_recv_data(client.data_to_send())\n        server.receive_data()\n\n        # Send moderate response (within max frame size)\n        moderate_body = b'Y' * 8000\n        sock.clear_sent()\n        server.send_response(1, 200, [('content-length', str(len(moderate_body)))], moderate_body)\n\n        # Client receives response\n        events = client.receive_data(sock.get_sent_data())\n        data_events = [e for e in events if isinstance(e, h2.events.DataReceived)]\n        received_data = b''.join(e.data for e in data_events)\n        assert received_data == moderate_body\n\n\nclass TestSpecialCases:\n    \"\"\"Test special/edge cases.\"\"\"\n\n    def test_head_request(self):\n        \"\"\"Test HEAD request (no body in response).\"\"\"\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        server = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        server.initiate_connection()\n\n        client = create_h2_client()\n        sock.set_recv_data(client.data_to_send())\n        server.receive_data()\n        client.receive_data(sock.get_sent_data())\n\n        client.send_headers(1, [\n            (':method', 'HEAD'),\n            (':path', '/resource'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n        ], end_stream=True)\n        sock.set_recv_data(client.data_to_send())\n        requests = server.receive_data()\n\n        assert requests[0].method == 'HEAD'\n\n        # Send response with content-length but no body\n        sock.clear_sent()\n        server.send_response(\n            1, 200,\n            [('content-length', '1000'), ('content-type', 'text/html')],\n            body=None\n        )\n\n        events = client.receive_data(sock.get_sent_data())\n        stream_ended = [e for e in events if isinstance(e, h2.events.StreamEnded)]\n        assert len(stream_ended) == 1\n\n    def test_options_request(self):\n        \"\"\"Test OPTIONS request.\"\"\"\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        server = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        server.initiate_connection()\n\n        client = create_h2_client()\n        sock.set_recv_data(client.data_to_send())\n        server.receive_data()\n        client.receive_data(sock.get_sent_data())\n\n        client.send_headers(1, [\n            (':method', 'OPTIONS'),\n            (':path', '*'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n        ], end_stream=True)\n        sock.set_recv_data(client.data_to_send())\n        requests = server.receive_data()\n\n        assert requests[0].method == 'OPTIONS'\n        assert requests[0].uri == '*'\n\n    def test_request_with_query_string(self):\n        \"\"\"Test request with query string parameters.\"\"\"\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        server = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        server.initiate_connection()\n\n        client = create_h2_client()\n        sock.set_recv_data(client.data_to_send())\n        server.receive_data()\n        client.receive_data(sock.get_sent_data())\n\n        client.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/search?q=test&page=2&sort=desc'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n        ], end_stream=True)\n        sock.set_recv_data(client.data_to_send())\n        requests = server.receive_data()\n\n        req = requests[0]\n        assert req.path == '/search'\n        assert req.query == 'q=test&page=2&sort=desc'\n\n    def test_request_with_multiple_headers_same_name(self):\n        \"\"\"Test request with multiple headers of the same name.\"\"\"\n        from gunicorn.http2.connection import HTTP2ServerConnection\n\n        cfg = MockConfig()\n        sock = MockSocket()\n        server = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345))\n        server.initiate_connection()\n\n        client = create_h2_client()\n        sock.set_recv_data(client.data_to_send())\n        server.receive_data()\n        client.receive_data(sock.get_sent_data())\n\n        client.send_headers(1, [\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n            ('accept', 'text/html'),\n            ('accept', 'application/json'),\n            ('accept', '*/*'),\n        ], end_stream=True)\n        sock.set_recv_data(client.data_to_send())\n        requests = server.receive_data()\n\n        req = requests[0]\n        accept_headers = [h[1] for h in req.headers if h[0] == 'ACCEPT']\n        assert len(accept_headers) == 3\n"
  },
  {
    "path": "tests/test_http2_request.py",
    "content": "# -*- coding: utf-8 -\n#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Tests for HTTP/2 request and body classes.\"\"\"\n\nimport pytest\n\nfrom gunicorn.http2.request import HTTP2Request, HTTP2Body\nfrom gunicorn.http2.stream import HTTP2Stream\n\n\nclass MockConnection:\n    \"\"\"Mock HTTP/2 connection for testing.\"\"\"\n\n    def __init__(self, initial_window_size=65535):\n        self.initial_window_size = initial_window_size\n\n\nclass MockConfig:\n    \"\"\"Mock gunicorn configuration.\"\"\"\n\n    def __init__(self):\n        pass\n\n\nclass TestHTTP2Body:\n    \"\"\"Test HTTP2Body class.\"\"\"\n\n    def test_init_with_data(self):\n        body = HTTP2Body(b\"Hello, World!\")\n        assert len(body) == 13\n\n    def test_init_empty(self):\n        body = HTTP2Body(b\"\")\n        assert len(body) == 0\n\n    def test_read_all(self):\n        body = HTTP2Body(b\"Test data\")\n        assert body.read() == b\"Test data\"\n        assert body.read() == b\"\"  # Already consumed\n\n    def test_read_with_size(self):\n        body = HTTP2Body(b\"Hello, World!\")\n        assert body.read(5) == b\"Hello\"\n        assert body.read(2) == b\", \"\n        assert body.read(100) == b\"World!\"\n        assert body.read(1) == b\"\"\n\n    def test_read_none_size(self):\n        body = HTTP2Body(b\"Test\")\n        assert body.read(None) == b\"Test\"\n\n    def test_readline_basic(self):\n        body = HTTP2Body(b\"Line1\\nLine2\\nLine3\")\n        assert body.readline() == b\"Line1\\n\"\n        assert body.readline() == b\"Line2\\n\"\n        assert body.readline() == b\"Line3\"\n\n    def test_readline_with_size(self):\n        body = HTTP2Body(b\"Hello\\nWorld\")\n        assert body.readline(3) == b\"Hel\"\n        assert body.readline(10) == b\"lo\\n\"\n\n    def test_readline_no_newline(self):\n        body = HTTP2Body(b\"No newline here\")\n        assert body.readline() == b\"No newline here\"\n\n    def test_readline_empty(self):\n        body = HTTP2Body(b\"\")\n        assert body.readline() == b\"\"\n\n    def test_readline_crlf(self):\n        body = HTTP2Body(b\"Line1\\r\\nLine2\")\n        # BytesIO readline includes \\r\\n\n        assert body.readline() == b\"Line1\\r\\n\"\n\n    def test_readlines_basic(self):\n        body = HTTP2Body(b\"Line1\\nLine2\\nLine3\")\n        lines = body.readlines()\n        assert lines == [b\"Line1\\n\", b\"Line2\\n\", b\"Line3\"]\n\n    def test_readlines_with_hint(self):\n        body = HTTP2Body(b\"Line1\\nLine2\\nLine3\\nLine4\")\n        # Hint affects how many lines are returned\n        lines = body.readlines(hint=5)\n        assert len(lines) >= 1\n\n    def test_readlines_empty(self):\n        body = HTTP2Body(b\"\")\n        assert body.readlines() == []\n\n    def test_iter(self):\n        body = HTTP2Body(b\"Line1\\nLine2\\nLine3\")\n        lines = list(body)\n        assert lines == [b\"Line1\\n\", b\"Line2\\n\", b\"Line3\"]\n\n    def test_len(self):\n        body = HTTP2Body(b\"12345\")\n        assert len(body) == 5\n\n    def test_close(self):\n        body = HTTP2Body(b\"test\")\n        body.close()\n        # Should not raise\n        with pytest.raises(ValueError):\n            body.read()\n\n\nclass TestHTTP2BodyReadStrategies:\n    \"\"\"Test different reading strategies matching HTTP/1.x patterns.\"\"\"\n\n    def test_read_all_at_once(self):\n        data = b\"A\" * 1000\n        body = HTTP2Body(data)\n        result = body.read()\n        assert result == data\n\n    def test_read_chunked(self):\n        data = b\"A\" * 100\n        body = HTTP2Body(data)\n        chunks = []\n        while True:\n            chunk = body.read(10)\n            if not chunk:\n                break\n            chunks.append(chunk)\n        assert b\"\".join(chunks) == data\n        assert len(chunks) == 10\n\n    def test_read_byte_by_byte(self):\n        data = b\"Hello\"\n        body = HTTP2Body(data)\n        result = []\n        for _ in range(len(data)):\n            result.append(body.read(1))\n        assert b\"\".join(result) == data\n\n    def test_readline_all_lines(self):\n        data = b\"Line1\\nLine2\\nLine3\\n\"\n        body = HTTP2Body(data)\n        lines = []\n        while True:\n            line = body.readline()\n            if not line:\n                break\n            lines.append(line)\n        assert lines == [b\"Line1\\n\", b\"Line2\\n\", b\"Line3\\n\"]\n\n\nclass TestHTTP2Request:\n    \"\"\"Test HTTP2Request class.\"\"\"\n\n    def _make_stream(self, headers, body=b\"\"):\n        \"\"\"Helper to create a stream with headers and body.\"\"\"\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.receive_headers(headers, end_stream=(len(body) == 0))\n        if body:\n            stream.request_body.write(body)\n            stream.request_complete = True\n        return stream\n\n    def test_basic_get_request(self):\n        stream = self._make_stream([\n            (':method', 'GET'),\n            (':path', '/test'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n        ])\n        cfg = MockConfig()\n        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))\n\n        assert req.method == 'GET'\n        assert req.uri == '/test'\n        assert req.path == '/test'\n        assert req.scheme == 'https'\n        assert req.version == (2, 0)\n\n    def test_post_request_with_body(self):\n        stream = self._make_stream(\n            [\n                (':method', 'POST'),\n                (':path', '/submit'),\n                (':scheme', 'https'),\n                (':authority', 'api.example.com'),\n                ('content-type', 'application/json'),\n                ('content-length', '13'),\n            ],\n            body=b'{\"key\":\"val\"}'\n        )\n        cfg = MockConfig()\n        req = HTTP2Request(stream, cfg, ('192.168.1.1', 54321))\n\n        assert req.method == 'POST'\n        assert req.body.read() == b'{\"key\":\"val\"}'\n        assert req.content_type == 'application/json'\n        assert req.content_length == 13\n\n    def test_path_with_query_string(self):\n        stream = self._make_stream([\n            (':method', 'GET'),\n            (':path', '/search?q=test&page=1'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n        ])\n        cfg = MockConfig()\n        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))\n\n        assert req.path == '/search'\n        assert req.query == 'q=test&page=1'\n        assert req.uri == '/search?q=test&page=1'\n\n    def test_path_with_fragment(self):\n        stream = self._make_stream([\n            (':method', 'GET'),\n            (':path', '/page#section'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n        ])\n        cfg = MockConfig()\n        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))\n\n        assert req.path == '/page'\n        assert req.fragment == 'section'\n\n    def test_headers_uppercase_conversion(self):\n        \"\"\"HTTP/2 headers are lowercase, should be converted to uppercase.\"\"\"\n        stream = self._make_stream([\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n            ('content-type', 'text/html'),\n            ('accept-language', 'en-US'),\n            ('x-custom-header', 'custom-value'),\n        ])\n        cfg = MockConfig()\n        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))\n\n        header_names = [h[0] for h in req.headers]\n        assert 'CONTENT-TYPE' in header_names\n        assert 'ACCEPT-LANGUAGE' in header_names\n        assert 'X-CUSTOM-HEADER' in header_names\n\n    def test_host_header_from_authority(self):\n        \"\"\"Host header should be generated from :authority pseudo-header.\"\"\"\n        stream = self._make_stream([\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'test.example.com:8080'),\n        ])\n        cfg = MockConfig()\n        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))\n\n        host = req.get_header('HOST')\n        assert host == 'test.example.com:8080'\n\n    def test_authority_overrides_host_header(self):\n        \"\"\":authority MUST override Host header per RFC 9113 section 8.3.1.\"\"\"\n        stream = self._make_stream([\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'authority.example.com'),\n            ('host', 'explicit.example.com'),\n        ])\n        cfg = MockConfig()\n        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))\n\n        # Count HOST headers - should be exactly one, from :authority\n        host_headers = [h for h in req.headers if h[0] == 'HOST']\n        assert len(host_headers) == 1\n        assert host_headers[0][1] == 'authority.example.com'\n\n    def test_get_header_case_insensitive(self):\n        stream = self._make_stream([\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n            ('x-test-header', 'test-value'),\n        ])\n        cfg = MockConfig()\n        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))\n\n        assert req.get_header('X-TEST-HEADER') == 'test-value'\n        assert req.get_header('x-test-header') == 'test-value'\n        assert req.get_header('X-Test-Header') == 'test-value'\n\n    def test_get_header_not_found(self):\n        stream = self._make_stream([\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n        ])\n        cfg = MockConfig()\n        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))\n\n        assert req.get_header('X-Not-Exists') is None\n\n    def test_content_length_property(self):\n        stream = self._make_stream([\n            (':method', 'POST'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n            ('content-length', '42'),\n        ])\n        cfg = MockConfig()\n        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))\n\n        assert req.content_length == 42\n\n    def test_content_length_none_when_missing(self):\n        stream = self._make_stream([\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n        ])\n        cfg = MockConfig()\n        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))\n\n        assert req.content_length is None\n\n    def test_content_length_invalid_value(self):\n        stream = self._make_stream([\n            (':method', 'POST'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n            ('content-length', 'not-a-number'),\n        ])\n        cfg = MockConfig()\n        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))\n\n        assert req.content_length is None\n\n    def test_content_type_property(self):\n        stream = self._make_stream([\n            (':method', 'POST'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n            ('content-type', 'application/json; charset=utf-8'),\n        ])\n        cfg = MockConfig()\n        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))\n\n        assert req.content_type == 'application/json; charset=utf-8'\n\n    def test_content_type_none_when_missing(self):\n        stream = self._make_stream([\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n        ])\n        cfg = MockConfig()\n        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))\n\n        assert req.content_type is None\n\n\nclass TestHTTP2RequestConnectionState:\n    \"\"\"Test connection state methods.\"\"\"\n\n    def _make_stream(self, headers):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.receive_headers(headers, end_stream=True)\n        return stream\n\n    def test_should_close_default_false(self):\n        \"\"\"HTTP/2 connections are persistent by default.\"\"\"\n        stream = self._make_stream([\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n        ])\n        cfg = MockConfig()\n        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))\n\n        assert req.should_close() is False\n\n    def test_force_close(self):\n        stream = self._make_stream([\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n        ])\n        cfg = MockConfig()\n        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))\n\n        req.force_close()\n        assert req.should_close() is True\n        assert req.must_close is True\n\n\nclass TestHTTP2RequestTrailers:\n    \"\"\"Test request trailers handling.\"\"\"\n\n    def test_no_trailers(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.receive_headers([\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n        ], end_stream=True)\n\n        cfg = MockConfig()\n        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))\n\n        assert req.trailers == []\n\n    def test_with_trailers(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.receive_headers([\n            (':method', 'POST'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n        ], end_stream=False)\n        stream.state = stream.state  # Keep state\n        stream.trailers = [\n            ('grpc-status', '0'),\n            ('grpc-message', 'OK'),\n        ]\n\n        cfg = MockConfig()\n        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))\n\n        assert len(req.trailers) == 2\n        assert ('GRPC-STATUS', '0') in req.trailers\n        assert ('GRPC-MESSAGE', 'OK') in req.trailers\n\n\nclass TestHTTP2RequestMetadata:\n    \"\"\"Test request metadata properties.\"\"\"\n\n    def _make_stream(self, headers, stream_id=1):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=stream_id, connection=conn)\n        stream.receive_headers(headers, end_stream=True)\n        return stream\n\n    def test_version_is_http2(self):\n        stream = self._make_stream([\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n        ])\n        cfg = MockConfig()\n        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))\n\n        assert req.version == (2, 0)\n\n    def test_req_number_is_stream_id(self):\n        stream = self._make_stream([\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n        ], stream_id=5)\n        cfg = MockConfig()\n        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))\n\n        assert req.req_number == 5\n\n    def test_peer_addr(self):\n        stream = self._make_stream([\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n        ])\n        cfg = MockConfig()\n        req = HTTP2Request(stream, cfg, ('10.0.0.1', 54321))\n\n        assert req.peer_addr == ('10.0.0.1', 54321)\n        assert req.remote_addr == ('10.0.0.1', 54321)\n\n    def test_proxy_protocol_info_none(self):\n        \"\"\"HTTP/2 doesn't use proxy protocol through data stream.\"\"\"\n        stream = self._make_stream([\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n        ])\n        cfg = MockConfig()\n        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))\n\n        assert req.proxy_protocol_info is None\n\n\nclass TestHTTP2RequestRepr:\n    \"\"\"Test request string representation.\"\"\"\n\n    def test_repr_format(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=3, connection=conn)\n        stream.receive_headers([\n            (':method', 'POST'),\n            (':path', '/api/users'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n        ], end_stream=True)\n\n        cfg = MockConfig()\n        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))\n\n        repr_str = repr(req)\n        assert \"HTTP2Request\" in repr_str\n        assert \"method=POST\" in repr_str\n        assert \"path=/api/users\" in repr_str\n        assert \"stream_id=3\" in repr_str\n\n\nclass TestHTTP2RequestDefaults:\n    \"\"\"Test default values when pseudo-headers are missing.\"\"\"\n\n    def test_default_method(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.receive_headers([\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n        ], end_stream=True)\n\n        cfg = MockConfig()\n        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))\n\n        assert req.method == 'GET'\n\n    def test_default_scheme(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.receive_headers([\n            (':method', 'GET'),\n            (':path', '/'),\n            (':authority', 'example.com'),\n        ], end_stream=True)\n\n        cfg = MockConfig()\n        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))\n\n        assert req.scheme == 'https'\n\n    def test_default_path(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.receive_headers([\n            (':method', 'GET'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n        ], end_stream=True)\n\n        cfg = MockConfig()\n        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))\n\n        assert req.uri == '/'\n        assert req.path == '/'\n\n\nclass TestHTTP2RequestPriority:\n    \"\"\"Test HTTP2Request priority attributes.\"\"\"\n\n    def test_default_priority_values(self):\n        \"\"\"Test that request inherits default stream priority.\"\"\"\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.receive_headers([\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n        ], end_stream=True)\n\n        cfg = MockConfig()\n        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))\n\n        assert req.priority_weight == 16\n        assert req.priority_depends_on == 0\n\n    def test_custom_priority_values(self):\n        \"\"\"Test that request inherits custom stream priority.\"\"\"\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=3, connection=conn)\n\n        # Update priority before creating request\n        stream.update_priority(weight=200, depends_on=1)\n\n        stream.receive_headers([\n            (':method', 'POST'),\n            (':path', '/api/data'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n        ], end_stream=False)\n        stream.receive_data(b'{\"data\": \"test\"}', end_stream=True)\n\n        cfg = MockConfig()\n        req = HTTP2Request(stream, cfg, ('192.168.1.100', 54321))\n\n        assert req.priority_weight == 200\n        assert req.priority_depends_on == 1\n\n    def test_priority_reflects_stream_at_request_creation(self):\n        \"\"\"Test that priority reflects stream state when request is created.\"\"\"\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.receive_headers([\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n        ], end_stream=True)\n\n        cfg = MockConfig()\n\n        # Create request with default priority\n        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))\n        assert req.priority_weight == 16\n\n        # Update stream priority after request was created\n        stream.update_priority(weight=256)\n\n        # Request should still have old value (captured at creation time)\n        assert req.priority_weight == 16\n\n        # Stream has new value\n        assert stream.priority_weight == 256\n\n\nclass MockWSGIConfig:\n    \"\"\"Mock gunicorn configuration with WSGI-required attributes.\"\"\"\n\n    def __init__(self):\n        self.errorlog = '-'\n        self.workers = 1\n\n\nclass TestHTTP2RequestWSGIEnviron:\n    \"\"\"Test HTTP/2 priority in WSGI environ.\"\"\"\n\n    def test_priority_in_wsgi_environ(self):\n        \"\"\"Test that HTTP/2 priority is added to WSGI environ.\"\"\"\n        from unittest import mock\n        from gunicorn.http.wsgi import create\n\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.update_priority(weight=128, depends_on=3)\n        stream.receive_headers([\n            (':method', 'GET'),\n            (':path', '/test'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n        ], end_stream=True)\n\n        cfg = MockConfig()\n        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))\n\n        # Create a mock socket\n        mock_sock = mock.Mock()\n        mock_sock.getsockname.return_value = ('127.0.0.1', 8443)\n\n        # Use WSGI config for environ creation\n        wsgi_cfg = MockWSGIConfig()\n\n        # Create WSGI environ\n        resp, environ = create(req, mock_sock, ('127.0.0.1', 12345), ('127.0.0.1', 8443), wsgi_cfg)\n\n        # Verify priority is in environ\n        assert environ.get('gunicorn.http2.priority_weight') == 128\n        assert environ.get('gunicorn.http2.priority_depends_on') == 3\n\n    def test_priority_not_in_environ_for_http1(self):\n        \"\"\"Test that HTTP/1 requests don't have priority keys.\"\"\"\n        from unittest import mock\n        from gunicorn.http.wsgi import create\n\n        # Create a mock HTTP/1 request (no priority attributes)\n        mock_req = mock.Mock()\n        mock_req.headers = [('HOST', 'example.com')]\n        mock_req.scheme = 'https'\n        mock_req.path = '/test'\n        mock_req.query = ''\n        mock_req.fragment = ''\n        mock_req.method = 'GET'\n        mock_req.uri = '/test'\n        mock_req.version = (1, 1)\n        mock_req._expected_100_continue = False\n        mock_req.proxy_protocol_info = None\n        mock_req.body = mock.Mock()\n\n        # Remove priority attributes to simulate HTTP/1 request\n        del mock_req.priority_weight\n        del mock_req.priority_depends_on\n\n        wsgi_cfg = MockWSGIConfig()\n\n        mock_sock = mock.Mock()\n        mock_sock.getsockname.return_value = ('127.0.0.1', 8443)\n\n        resp, environ = create(mock_req, mock_sock, ('127.0.0.1', 12345), ('127.0.0.1', 8443), wsgi_cfg)\n\n        # HTTP/1 requests should not have priority keys\n        assert 'gunicorn.http2.priority_weight' not in environ\n        assert 'gunicorn.http2.priority_depends_on' not in environ\n"
  },
  {
    "path": "tests/test_http2_stream.py",
    "content": "# -*- coding: utf-8 -\n#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"Tests for HTTP/2 stream state management.\"\"\"\n\nimport pytest\n\nfrom gunicorn.http2.stream import HTTP2Stream, StreamState\nfrom gunicorn.http2.errors import HTTP2StreamError\n\n\nclass MockConnection:\n    \"\"\"Mock HTTP/2 connection for testing streams.\"\"\"\n\n    def __init__(self, initial_window_size=65535):\n        self.initial_window_size = initial_window_size\n\n\nclass TestStreamState:\n    \"\"\"Test StreamState enum values.\"\"\"\n\n    def test_state_values_exist(self):\n        assert StreamState.IDLE is not None\n        assert StreamState.RESERVED_LOCAL is not None\n        assert StreamState.RESERVED_REMOTE is not None\n        assert StreamState.OPEN is not None\n        assert StreamState.HALF_CLOSED_LOCAL is not None\n        assert StreamState.HALF_CLOSED_REMOTE is not None\n        assert StreamState.CLOSED is not None\n\n    def test_states_are_unique(self):\n        states = [\n            StreamState.IDLE,\n            StreamState.RESERVED_LOCAL,\n            StreamState.RESERVED_REMOTE,\n            StreamState.OPEN,\n            StreamState.HALF_CLOSED_LOCAL,\n            StreamState.HALF_CLOSED_REMOTE,\n            StreamState.CLOSED,\n        ]\n        assert len(states) == len(set(states))\n\n\nclass TestHTTP2StreamInitialization:\n    \"\"\"Test stream initialization.\"\"\"\n\n    def test_basic_init(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n\n        assert stream.stream_id == 1\n        assert stream.connection is conn\n        assert stream.state == StreamState.IDLE\n        assert stream.request_headers == []\n        assert stream.request_complete is False\n        assert stream.response_started is False\n        assert stream.response_headers_sent is False\n        assert stream.response_complete is False\n        assert stream.window_size == 65535\n        assert stream.trailers is None\n\n    def test_custom_window_size(self):\n        conn = MockConnection(initial_window_size=32768)\n        stream = HTTP2Stream(stream_id=3, connection=conn)\n        assert stream.window_size == 32768\n\n\nclass TestStreamIdProperties:\n    \"\"\"Test stream ID classification properties.\"\"\"\n\n    def test_is_client_stream_odd_ids(self):\n        conn = MockConnection()\n        for stream_id in [1, 3, 5, 7, 99, 101]:\n            stream = HTTP2Stream(stream_id=stream_id, connection=conn)\n            assert stream.is_client_stream is True\n            assert stream.is_server_stream is False\n\n    def test_is_server_stream_even_ids(self):\n        conn = MockConnection()\n        for stream_id in [2, 4, 6, 8, 100, 102]:\n            stream = HTTP2Stream(stream_id=stream_id, connection=conn)\n            assert stream.is_client_stream is False\n            assert stream.is_server_stream is True\n\n    def test_stream_id_zero(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=0, connection=conn)\n        assert stream.is_client_stream is False\n        assert stream.is_server_stream is True\n\n\nclass TestCanReceiveProperty:\n    \"\"\"Test can_receive property.\"\"\"\n\n    def test_can_receive_in_open_state(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.OPEN\n        assert stream.can_receive is True\n\n    def test_can_receive_in_half_closed_local(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.HALF_CLOSED_LOCAL\n        assert stream.can_receive is True\n\n    def test_cannot_receive_in_idle(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        assert stream.state == StreamState.IDLE\n        assert stream.can_receive is False\n\n    def test_cannot_receive_in_half_closed_remote(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.HALF_CLOSED_REMOTE\n        assert stream.can_receive is False\n\n    def test_cannot_receive_in_closed(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.CLOSED\n        assert stream.can_receive is False\n\n\nclass TestCanSendProperty:\n    \"\"\"Test can_send property.\"\"\"\n\n    def test_can_send_in_open_state(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.OPEN\n        assert stream.can_send is True\n\n    def test_can_send_in_half_closed_remote(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.HALF_CLOSED_REMOTE\n        assert stream.can_send is True\n\n    def test_cannot_send_in_idle(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        assert stream.state == StreamState.IDLE\n        assert stream.can_send is False\n\n    def test_cannot_send_in_half_closed_local(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.HALF_CLOSED_LOCAL\n        assert stream.can_send is False\n\n    def test_cannot_send_in_closed(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.CLOSED\n        assert stream.can_send is False\n\n\nclass TestReceiveHeaders:\n    \"\"\"Test receive_headers method.\"\"\"\n\n    def test_receive_headers_from_idle(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        headers = [(':method', 'GET'), (':path', '/')]\n\n        stream.receive_headers(headers, end_stream=False)\n\n        assert stream.state == StreamState.OPEN\n        assert stream.request_headers == headers\n        assert stream.request_complete is False\n\n    def test_receive_headers_with_end_stream(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        headers = [(':method', 'GET'), (':path', '/')]\n\n        stream.receive_headers(headers, end_stream=True)\n\n        assert stream.state == StreamState.HALF_CLOSED_REMOTE\n        assert stream.request_complete is True\n\n    def test_receive_headers_in_open_state(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.OPEN\n\n        headers = [('content-type', 'text/plain')]\n        stream.receive_headers(headers, end_stream=False)\n\n        assert stream.state == StreamState.OPEN\n        assert stream.request_headers == headers\n\n    def test_receive_headers_extends_existing(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.receive_headers([(':method', 'POST')], end_stream=False)\n        stream.receive_headers([('content-type', 'text/plain')], end_stream=False)\n\n        assert len(stream.request_headers) == 2\n\n    def test_receive_headers_in_invalid_state(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.CLOSED\n\n        with pytest.raises(HTTP2StreamError) as exc_info:\n            stream.receive_headers([], end_stream=False)\n        assert exc_info.value.stream_id == 1\n\n\nclass TestReceiveData:\n    \"\"\"Test receive_data method.\"\"\"\n\n    def test_receive_data_in_open_state(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.OPEN\n\n        stream.receive_data(b\"Hello, World!\", end_stream=False)\n\n        assert stream.request_body.getvalue() == b\"Hello, World!\"\n        assert stream.request_complete is False\n\n    def test_receive_data_with_end_stream(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.OPEN\n\n        stream.receive_data(b\"Final data\", end_stream=True)\n\n        assert stream.state == StreamState.HALF_CLOSED_REMOTE\n        assert stream.request_complete is True\n\n    def test_receive_data_accumulates(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.OPEN\n\n        stream.receive_data(b\"Part1\")\n        stream.receive_data(b\"Part2\")\n        stream.receive_data(b\"Part3\", end_stream=True)\n\n        assert stream.request_body.getvalue() == b\"Part1Part2Part3\"\n\n    def test_receive_data_in_half_closed_local(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.HALF_CLOSED_LOCAL\n\n        stream.receive_data(b\"data\", end_stream=False)\n        assert stream.request_body.getvalue() == b\"data\"\n\n    def test_receive_data_in_invalid_state(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.HALF_CLOSED_REMOTE\n\n        with pytest.raises(HTTP2StreamError) as exc_info:\n            stream.receive_data(b\"data\", end_stream=False)\n        assert exc_info.value.stream_id == 1\n\n\nclass TestReceiveTrailers:\n    \"\"\"Test receive_trailers method.\"\"\"\n\n    def test_receive_trailers_in_open_state(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.OPEN\n\n        trailers = [('grpc-status', '0')]\n        stream.receive_trailers(trailers)\n\n        assert stream.trailers == trailers\n        assert stream.state == StreamState.HALF_CLOSED_REMOTE\n        assert stream.request_complete is True\n\n    def test_receive_trailers_in_invalid_state(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.CLOSED\n\n        with pytest.raises(HTTP2StreamError):\n            stream.receive_trailers([])\n\n\nclass TestSendHeaders:\n    \"\"\"Test send_headers method.\"\"\"\n\n    def test_send_headers_in_open_state(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.OPEN\n\n        headers = [(':status', '200')]\n        stream.send_headers(headers, end_stream=False)\n\n        assert stream.response_started is True\n        assert stream.response_headers_sent is True\n        assert stream.response_complete is False\n        assert stream.state == StreamState.OPEN\n\n    def test_send_headers_with_end_stream(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.OPEN\n\n        stream.send_headers([(':status', '204')], end_stream=True)\n\n        assert stream.state == StreamState.HALF_CLOSED_LOCAL\n        assert stream.response_complete is True\n\n    def test_send_headers_in_half_closed_remote(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.HALF_CLOSED_REMOTE\n\n        stream.send_headers([(':status', '200')], end_stream=False)\n        assert stream.response_headers_sent is True\n\n    def test_send_headers_in_invalid_state(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.HALF_CLOSED_LOCAL\n\n        with pytest.raises(HTTP2StreamError):\n            stream.send_headers([], end_stream=False)\n\n\nclass TestSendData:\n    \"\"\"Test send_data method.\"\"\"\n\n    def test_send_data_in_open_state(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.OPEN\n\n        stream.send_data(b\"Response body\", end_stream=False)\n        assert stream.response_complete is False\n\n    def test_send_data_with_end_stream(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.OPEN\n\n        stream.send_data(b\"Final\", end_stream=True)\n\n        assert stream.state == StreamState.HALF_CLOSED_LOCAL\n        assert stream.response_complete is True\n\n    def test_send_data_in_half_closed_remote(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.HALF_CLOSED_REMOTE\n\n        stream.send_data(b\"data\", end_stream=True)\n        assert stream.state == StreamState.CLOSED\n\n    def test_send_data_in_invalid_state(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.CLOSED\n\n        with pytest.raises(HTTP2StreamError):\n            stream.send_data(b\"data\", end_stream=False)\n\n\nclass TestStreamReset:\n    \"\"\"Test stream reset method.\"\"\"\n\n    def test_reset_default_error_code(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.OPEN\n\n        stream.reset()\n\n        assert stream.state == StreamState.CLOSED\n        assert stream.response_complete is True\n        assert stream.request_complete is True\n\n    def test_reset_custom_error_code(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.OPEN\n\n        stream.reset(error_code=0x1)  # PROTOCOL_ERROR\n\n        assert stream.state == StreamState.CLOSED\n\n\nclass TestStreamClose:\n    \"\"\"Test stream close method.\"\"\"\n\n    def test_close_stream(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.OPEN\n\n        stream.close()\n\n        assert stream.state == StreamState.CLOSED\n        assert stream.response_complete is True\n        assert stream.request_complete is True\n\n\nclass TestHalfCloseTransitions:\n    \"\"\"Test half-close state transitions.\"\"\"\n\n    def test_half_close_local_from_open(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.OPEN\n\n        stream._half_close_local()\n        assert stream.state == StreamState.HALF_CLOSED_LOCAL\n\n    def test_half_close_local_from_half_closed_remote(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.HALF_CLOSED_REMOTE\n\n        stream._half_close_local()\n        assert stream.state == StreamState.CLOSED\n\n    def test_half_close_local_invalid_state(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.IDLE\n\n        with pytest.raises(HTTP2StreamError):\n            stream._half_close_local()\n\n    def test_half_close_remote_from_open(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.OPEN\n\n        stream._half_close_remote()\n        assert stream.state == StreamState.HALF_CLOSED_REMOTE\n\n    def test_half_close_remote_from_half_closed_local(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.HALF_CLOSED_LOCAL\n\n        stream._half_close_remote()\n        assert stream.state == StreamState.CLOSED\n\n    def test_half_close_remote_invalid_state(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.IDLE\n\n        with pytest.raises(HTTP2StreamError):\n            stream._half_close_remote()\n\n\nclass TestGetRequestBody:\n    \"\"\"Test get_request_body method.\"\"\"\n\n    def test_get_empty_body(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        assert stream.get_request_body() == b\"\"\n\n    def test_get_body_after_data(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.state = StreamState.OPEN\n        stream.receive_data(b\"Test body content\")\n\n        assert stream.get_request_body() == b\"Test body content\"\n\n\nclass TestGetPseudoHeaders:\n    \"\"\"Test get_pseudo_headers method.\"\"\"\n\n    def test_extract_pseudo_headers(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.request_headers = [\n            (':method', 'POST'),\n            (':path', '/api/test'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n            ('content-type', 'application/json'),\n            ('accept', '*/*'),\n        ]\n\n        pseudo = stream.get_pseudo_headers()\n\n        assert pseudo == {\n            ':method': 'POST',\n            ':path': '/api/test',\n            ':scheme': 'https',\n            ':authority': 'example.com',\n        }\n\n    def test_empty_pseudo_headers(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.request_headers = [\n            ('content-type', 'text/plain'),\n        ]\n\n        pseudo = stream.get_pseudo_headers()\n        assert pseudo == {}\n\n\nclass TestGetRegularHeaders:\n    \"\"\"Test get_regular_headers method.\"\"\"\n\n    def test_extract_regular_headers(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.request_headers = [\n            (':method', 'GET'),\n            (':path', '/'),\n            ('content-type', 'text/html'),\n            ('accept-language', 'en-US'),\n        ]\n\n        regular = stream.get_regular_headers()\n\n        assert regular == [\n            ('content-type', 'text/html'),\n            ('accept-language', 'en-US'),\n        ]\n\n    def test_no_regular_headers(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        stream.request_headers = [\n            (':method', 'GET'),\n            (':path', '/'),\n        ]\n\n        regular = stream.get_regular_headers()\n        assert regular == []\n\n\nclass TestStreamRepr:\n    \"\"\"Test stream string representation.\"\"\"\n\n    def test_repr_format(self):\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=5, connection=conn)\n        repr_str = repr(stream)\n\n        assert \"HTTP2Stream\" in repr_str\n        assert \"id=5\" in repr_str\n        assert \"state=IDLE\" in repr_str\n        assert \"req_complete=False\" in repr_str\n        assert \"resp_complete=False\" in repr_str\n\n\nclass TestFullStreamLifecycle:\n    \"\"\"Test complete stream lifecycles.\"\"\"\n\n    def test_simple_get_request(self):\n        \"\"\"Test a simple GET request lifecycle.\"\"\"\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n\n        # Receive request headers (GET with end_stream)\n        stream.receive_headers([\n            (':method', 'GET'),\n            (':path', '/'),\n            (':scheme', 'https'),\n            (':authority', 'example.com'),\n        ], end_stream=True)\n\n        assert stream.state == StreamState.HALF_CLOSED_REMOTE\n        assert stream.request_complete is True\n\n        # Send response headers with body\n        stream.send_headers([(':status', '200')], end_stream=False)\n        assert stream.state == StreamState.HALF_CLOSED_REMOTE\n\n        # Send response body\n        stream.send_data(b\"Hello!\", end_stream=True)\n        assert stream.state == StreamState.CLOSED\n        assert stream.response_complete is True\n\n    def test_post_request_with_body(self):\n        \"\"\"Test a POST request with body.\"\"\"\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n\n        # Receive request headers\n        stream.receive_headers([\n            (':method', 'POST'),\n            (':path', '/submit'),\n            ('content-type', 'application/json'),\n        ], end_stream=False)\n\n        assert stream.state == StreamState.OPEN\n\n        # Receive body data\n        stream.receive_data(b'{\"key\": \"value\"}', end_stream=True)\n        assert stream.state == StreamState.HALF_CLOSED_REMOTE\n        assert stream.get_request_body() == b'{\"key\": \"value\"}'\n\n        # Send response\n        stream.send_headers([(':status', '201')], end_stream=False)\n        stream.send_data(b'Created', end_stream=True)\n\n        assert stream.state == StreamState.CLOSED\n\n    def test_stream_reset_lifecycle(self):\n        \"\"\"Test a stream that gets reset.\"\"\"\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n\n        stream.receive_headers([(':method', 'GET'), (':path', '/')], end_stream=False)\n        assert stream.state == StreamState.OPEN\n\n        # Reset the stream\n        stream.reset(error_code=0x8)  # CANCEL\n\n        assert stream.state == StreamState.CLOSED\n        assert stream.request_complete is True\n        assert stream.response_complete is True\n\n\nclass TestStreamPriority:\n    \"\"\"Test stream priority support (RFC 7540 Section 5.3).\"\"\"\n\n    def test_default_priority_values(self):\n        \"\"\"Test default priority values.\"\"\"\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n\n        assert stream.priority_weight == 16\n        assert stream.priority_depends_on == 0\n        assert stream.priority_exclusive is False\n\n    def test_update_priority_weight(self):\n        \"\"\"Test updating priority weight.\"\"\"\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n\n        stream.update_priority(weight=256)\n        assert stream.priority_weight == 256\n\n        stream.update_priority(weight=1)\n        assert stream.priority_weight == 1\n\n    def test_update_priority_depends_on(self):\n        \"\"\"Test updating priority dependency.\"\"\"\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=3, connection=conn)\n\n        stream.update_priority(depends_on=1)\n        assert stream.priority_depends_on == 1\n\n    def test_update_priority_exclusive(self):\n        \"\"\"Test updating exclusive flag.\"\"\"\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=3, connection=conn)\n\n        stream.update_priority(exclusive=True)\n        assert stream.priority_exclusive is True\n\n        stream.update_priority(exclusive=False)\n        assert stream.priority_exclusive is False\n\n    def test_update_priority_all_fields(self):\n        \"\"\"Test updating all priority fields at once.\"\"\"\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=5, connection=conn)\n\n        stream.update_priority(weight=128, depends_on=1, exclusive=True)\n\n        assert stream.priority_weight == 128\n        assert stream.priority_depends_on == 1\n        assert stream.priority_exclusive is True\n\n    def test_update_priority_partial(self):\n        \"\"\"Test that partial updates don't affect other fields.\"\"\"\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n\n        # Set initial values\n        stream.update_priority(weight=200, depends_on=3, exclusive=True)\n\n        # Update only weight\n        stream.update_priority(weight=100)\n        assert stream.priority_weight == 100\n        assert stream.priority_depends_on == 3  # unchanged\n        assert stream.priority_exclusive is True  # unchanged\n\n    def test_weight_clamped_to_min(self):\n        \"\"\"Test that weight is clamped to minimum of 1.\"\"\"\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n\n        stream.update_priority(weight=0)\n        assert stream.priority_weight == 1\n\n        stream.update_priority(weight=-10)\n        assert stream.priority_weight == 1\n\n    def test_weight_clamped_to_max(self):\n        \"\"\"Test that weight is clamped to maximum of 256.\"\"\"\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n\n        stream.update_priority(weight=300)\n        assert stream.priority_weight == 256\n\n        stream.update_priority(weight=1000)\n        assert stream.priority_weight == 256\n\n\nclass TestStreamResponseTrailers:\n    \"\"\"Test response trailer support.\"\"\"\n\n    def test_response_trailers_default_none(self):\n        \"\"\"Test that response_trailers defaults to None.\"\"\"\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n        assert stream.response_trailers is None\n\n    def test_send_trailers_in_open_state(self):\n        \"\"\"Test sending trailers in OPEN state.\"\"\"\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n\n        # Open the stream\n        stream.receive_headers([(':method', 'GET'), (':path', '/')], end_stream=True)\n        assert stream.state == StreamState.HALF_CLOSED_REMOTE\n\n        # Send response headers\n        stream.send_headers([(':status', '200')], end_stream=False)\n\n        # Send trailers\n        trailers = [('grpc-status', '0'), ('grpc-message', 'OK')]\n        stream.send_trailers(trailers)\n\n        assert stream.response_trailers == trailers\n        assert stream.state == StreamState.CLOSED\n        assert stream.response_complete is True\n\n    def test_send_trailers_after_body(self):\n        \"\"\"Test sending trailers after response body.\"\"\"\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n\n        # Open the stream\n        stream.receive_headers([(':method', 'POST'), (':path', '/api')], end_stream=False)\n        stream.receive_data(b'request body', end_stream=True)\n\n        # Send response\n        stream.send_headers([(':status', '200')], end_stream=False)\n        stream.send_data(b'response body', end_stream=False)\n\n        # Send trailers\n        trailers = [('content-md5', 'abc123')]\n        stream.send_trailers(trailers)\n\n        assert stream.response_trailers == trailers\n        assert stream.state == StreamState.CLOSED\n\n    def test_send_trailers_closes_stream(self):\n        \"\"\"Test that trailers close the stream.\"\"\"\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n\n        stream.receive_headers([(':method', 'GET'), (':path', '/')], end_stream=True)\n        stream.send_headers([(':status', '200')], end_stream=False)\n\n        assert stream.can_send is True\n\n        stream.send_trailers([('trailer', 'value')])\n\n        assert stream.can_send is False\n        assert stream.response_complete is True\n\n    def test_send_trailers_invalid_state_raises(self):\n        \"\"\"Test that sending trailers in invalid state raises error.\"\"\"\n        conn = MockConnection()\n        stream = HTTP2Stream(stream_id=1, connection=conn)\n\n        # Stream is IDLE, cannot send trailers\n        with pytest.raises(HTTP2StreamError):\n            stream.send_trailers([('trailer', 'value')])\n"
  },
  {
    "path": "tests/test_invalid_requests.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport glob\nimport os\n\nimport pytest\n\nimport treq\n\ndirname = os.path.dirname(__file__)\nreqdir = os.path.join(dirname, \"requests\", \"invalid\")\nhttpfiles = glob.glob(os.path.join(reqdir, \"*.http\"))\n\n\n@pytest.mark.parametrize(\"fname\", httpfiles)\ndef test_http_parser(fname):\n    env = treq.load_py(os.path.splitext(fname)[0] + \".py\")\n\n    expect = env[\"request\"]\n    cfg = env[\"cfg\"]\n    req = treq.badrequest(fname)\n\n    with pytest.raises(expect):\n        req.check(cfg)\n"
  },
  {
    "path": "tests/test_logger.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport datetime\nfrom types import SimpleNamespace\n\nimport pytest\n\nfrom gunicorn.config import Config\nfrom gunicorn.glogging import Logger\n\n\ndef test_atoms_defaults():\n    response = SimpleNamespace(\n        status='200', response_length=1024,\n        headers=(('Content-Type', 'application/json'),), sent=1024,\n    )\n    request = SimpleNamespace(headers=(('Accept', 'application/json'),))\n    environ = {\n        'REQUEST_METHOD': 'GET', 'RAW_URI': '/my/path?foo=bar',\n        'PATH_INFO': '/my/path', 'QUERY_STRING': 'foo=bar',\n        'SERVER_PROTOCOL': 'HTTP/1.1',\n    }\n    logger = Logger(Config())\n    atoms = logger.atoms(response, request, environ, datetime.timedelta(seconds=1))\n    assert isinstance(atoms, dict)\n    assert atoms['r'] == 'GET /my/path?foo=bar HTTP/1.1'\n    assert atoms['m'] == 'GET'\n    assert atoms['U'] == '/my/path'\n    assert atoms['q'] == 'foo=bar'\n    assert atoms['H'] == 'HTTP/1.1'\n    assert atoms['b'] == '1024'\n    assert atoms['B'] == 1024\n    assert atoms['{accept}i'] == 'application/json'\n    assert atoms['{content-type}o'] == 'application/json'\n\n\ndef test_atoms_zero_bytes():\n    response = SimpleNamespace(\n        status='200', response_length=0,\n        headers=(('Content-Type', 'application/json'),), sent=0,\n    )\n    request = SimpleNamespace(headers=(('Accept', 'application/json'),))\n    environ = {\n        'REQUEST_METHOD': 'GET', 'RAW_URI': '/my/path?foo=bar',\n        'PATH_INFO': '/my/path', 'QUERY_STRING': 'foo=bar',\n        'SERVER_PROTOCOL': 'HTTP/1.1',\n    }\n    logger = Logger(Config())\n    atoms = logger.atoms(response, request, environ, datetime.timedelta(seconds=1))\n    assert atoms['b'] == '0'\n    assert atoms['B'] == 0\n\n\n@pytest.mark.parametrize('auth', [\n    # auth type is case in-sensitive\n    'Basic YnJrMHY6',\n    'basic YnJrMHY6',\n    'BASIC YnJrMHY6',\n])\ndef test_get_username_from_basic_auth_header(auth):\n    request = SimpleNamespace(headers=())\n    response = SimpleNamespace(\n        status='200', response_length=1024, sent=1024,\n        headers=(('Content-Type', 'text/plain'),),\n    )\n    environ = {\n        'REQUEST_METHOD': 'GET', 'RAW_URI': '/my/path?foo=bar',\n        'PATH_INFO': '/my/path', 'QUERY_STRING': 'foo=bar',\n        'SERVER_PROTOCOL': 'HTTP/1.1',\n        'HTTP_AUTHORIZATION': auth,\n    }\n    logger = Logger(Config())\n    atoms = logger.atoms(response, request, environ, datetime.timedelta(seconds=1))\n    assert atoms['u'] == 'brk0v'\n\n\ndef test_get_username_handles_malformed_basic_auth_header():\n    \"\"\"Should catch a malformed auth header\"\"\"\n    request = SimpleNamespace(headers=())\n    response = SimpleNamespace(\n        status='200', response_length=1024, sent=1024,\n        headers=(('Content-Type', 'text/plain'),),\n    )\n    environ = {\n        'REQUEST_METHOD': 'GET', 'RAW_URI': '/my/path?foo=bar',\n        'PATH_INFO': '/my/path', 'QUERY_STRING': 'foo=bar',\n        'SERVER_PROTOCOL': 'HTTP/1.1',\n        'HTTP_AUTHORIZATION': 'Basic ixsTtkKzIpVTncfQjbBcnoRNoDfbnaXG',\n    }\n    logger = Logger(Config())\n\n    atoms = logger.atoms(response, request, environ, datetime.timedelta(seconds=1))\n    assert atoms['u'] == '-'\n"
  },
  {
    "path": "tests/test_pidfile.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport errno\nfrom unittest import mock\n\nimport gunicorn.pidfile\n\n\ndef builtin(name):\n    return 'builtins.{}'.format(name)\n\n\n@mock.patch(builtin('open'), new_callable=mock.mock_open)\ndef test_validate_no_file(_open):\n    pidfile = gunicorn.pidfile.Pidfile('test.pid')\n    _open.side_effect = IOError(errno.ENOENT)\n    assert pidfile.validate() is None\n\n\n@mock.patch(builtin('open'), new_callable=mock.mock_open, read_data='1')\n@mock.patch('os.kill')\ndef test_validate_file_pid_exists(kill, _open):\n    pidfile = gunicorn.pidfile.Pidfile('test.pid')\n    assert pidfile.validate() == 1\n    assert kill.called\n\n\n@mock.patch(builtin('open'), new_callable=mock.mock_open, read_data='a')\ndef test_validate_file_pid_malformed(_open):\n    pidfile = gunicorn.pidfile.Pidfile('test.pid')\n    assert pidfile.validate() is None\n\n\n@mock.patch(builtin('open'), new_callable=mock.mock_open, read_data='1')\n@mock.patch('os.kill')\ndef test_validate_file_pid_exists_kill_exception(kill, _open):\n    pidfile = gunicorn.pidfile.Pidfile('test.pid')\n    kill.side_effect = OSError(errno.EPERM)\n    assert pidfile.validate() == 1\n\n\n@mock.patch(builtin('open'), new_callable=mock.mock_open, read_data='1')\n@mock.patch('os.kill')\ndef test_validate_file_pid_does_not_exist(kill, _open):\n    pidfile = gunicorn.pidfile.Pidfile('test.pid')\n    kill.side_effect = OSError(errno.ESRCH)\n    assert pidfile.validate() is None\n"
  },
  {
    "path": "tests/test_reload.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport unittest.mock as mock\n\nfrom gunicorn.app.base import Application\nfrom gunicorn.workers.base import Worker\nfrom gunicorn.reloader import reloader_engines\n\n\nclass ReloadApp(Application):\n    def __init__(self):\n        super().__init__(\"no usage\", prog=\"gunicorn_test\")\n\n    def do_load_config(self):\n        self.load_default_config()\n        self.cfg.set('reload', True)\n        self.cfg.set('reload_engine', 'poll')\n\n\nclass SyntaxErrorApp(ReloadApp):\n    def wsgi(self):\n        error = SyntaxError('invalid syntax')\n        error.filename = 'syntax_error_filename'\n        raise error\n\n\nclass MyWorker(Worker):\n    def run(self):\n        pass\n\n\ndef test_reload_on_syntax_error():\n    \"\"\"\n    Test that reloading works if the application has a syntax error.\n    \"\"\"\n    reloader = mock.Mock()\n    reloader_engines['poll'] = lambda *args, **kw: reloader\n\n    app = SyntaxErrorApp()\n    cfg = app.cfg\n    log = mock.Mock()\n    worker = MyWorker(age=0, ppid=0, sockets=[], app=app, timeout=0, cfg=cfg, log=log)\n\n    worker.init_process()\n    reloader.start.assert_called_with()\n    reloader.add_extra_file.assert_called_with('syntax_error_filename')\n\n\ndef test_start_reloader_after_load_wsgi():\n    \"\"\"\n    Check that the reloader is started after the wsgi app has been loaded.\n    \"\"\"\n    reloader = mock.Mock()\n    reloader_engines['poll'] = lambda *args, **kw: reloader\n\n    app = ReloadApp()\n    cfg = app.cfg\n    log = mock.Mock()\n    worker = MyWorker(age=0, ppid=0, sockets=[], app=app, timeout=0, cfg=cfg, log=log)\n\n    worker.load_wsgi = mock.Mock()\n    mock_parent = mock.Mock()\n    mock_parent.attach_mock(worker.load_wsgi, 'load_wsgi')\n    mock_parent.attach_mock(reloader.start, 'reloader_start')\n\n    worker.init_process()\n    mock_parent.assert_has_calls([\n        mock.call.load_wsgi(),\n        mock.call.reloader_start(),\n    ])\n"
  },
  {
    "path": "tests/test_signal_integration.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\"\"\"\nIntegration tests for arbiter signal handling.\n\nThese tests start a real gunicorn process and verify signal handling\nworks correctly with actual requests and signals.\n\"\"\"\n\nimport os\nimport signal\nimport socket\nimport subprocess\nimport sys\nimport time\n\nimport pytest\n\n\n# Timeout for CI environments (VMs can be slow, PyPy needs more time)\nCI_TIMEOUT = 90\n\n\n# Simple WSGI app inline\nSIMPLE_APP = '''\ndef application(environ, start_response):\n    \"\"\"Basic hello world response.\"\"\"\n    status = '200 OK'\n    body = b'Hello, World!'\n    headers = [\n        ('Content-Type', 'text/plain'),\n        ('Content-Length', str(len(body))),\n    ]\n    start_response(status, headers)\n    return [body]\n'''\n\n\ndef find_free_port():\n    \"\"\"Find a free port to bind to.\"\"\"\n    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:\n        s.bind(('127.0.0.1', 0))\n        return s.getsockname()[1]\n\n\ndef wait_for_server(host, port, timeout=CI_TIMEOUT):\n    \"\"\"Wait until server is accepting connections.\"\"\"\n    start = time.monotonic()\n    while time.monotonic() - start < timeout:\n        try:\n            with socket.create_connection((host, port), timeout=1):\n                return True\n        except (ConnectionRefusedError, socket.timeout, OSError):\n            time.sleep(0.1)\n    return False\n\n\ndef make_request(host, port, path='/'):\n    \"\"\"Make a simple HTTP request and return the response body.\"\"\"\n    with socket.create_connection((host, port), timeout=5) as sock:\n        request = f'GET {path} HTTP/1.1\\r\\nHost: {host}\\r\\nConnection: close\\r\\n\\r\\n'\n        sock.sendall(request.encode())\n        response = b''\n        while True:\n            chunk = sock.recv(4096)\n            if not chunk:\n                break\n            response += chunk\n        return response\n\n\n@pytest.fixture\ndef app_module(tmp_path):\n    \"\"\"Create a temporary app module.\"\"\"\n    app_file = tmp_path / \"app.py\"\n    app_file.write_text(SIMPLE_APP)\n    return str(app_file.parent), \"app:application\"\n\n\n@pytest.fixture\ndef gunicorn_server(app_module):\n    \"\"\"Start and stop a gunicorn server.\"\"\"\n    app_dir, app_name = app_module\n    port = find_free_port()\n\n    # Start gunicorn\n    cmd = [\n        sys.executable, '-m', 'gunicorn',\n        '--bind', f'127.0.0.1:{port}',\n        '--workers', '2',\n        '--worker-class', 'sync',\n        '--access-logfile', '-',\n        '--error-logfile', '-',\n        '--log-level', 'info',\n        '--timeout', '30',\n        '--graceful-timeout', '30',\n        app_name\n    ]\n\n    # Use setsid to create new process group for proper signal handling\n    proc = subprocess.Popen(\n        cmd,\n        cwd=app_dir,\n        stdout=subprocess.PIPE,\n        stderr=subprocess.PIPE,\n        env={**os.environ, 'PYTHONPATH': app_dir},\n        preexec_fn=os.setsid\n    )\n\n    # Wait for server to start\n    if not wait_for_server('127.0.0.1', port):\n        proc.terminate()\n        proc.wait()\n        stdout, stderr = proc.communicate()\n        pytest.fail(f\"Gunicorn failed to start:\\nstdout: {stdout.decode()}\\nstderr: {stderr.decode()}\")\n\n    yield proc, port\n\n    # Cleanup - use process group kill for better cleanup\n    if proc.poll() is None:\n        try:\n            os.killpg(os.getpgid(proc.pid), signal.SIGTERM)\n        except (ProcessLookupError, OSError):\n            pass\n        try:\n            proc.wait(timeout=5)\n        except subprocess.TimeoutExpired:\n            try:\n                os.killpg(os.getpgid(proc.pid), signal.SIGKILL)\n            except (ProcessLookupError, OSError):\n                pass\n            proc.wait()\n\n\nclass TestSignalHandlingIntegration:\n    \"\"\"Integration tests for signal handling.\"\"\"\n\n    def test_basic_request(self, gunicorn_server):\n        \"\"\"Verify the server responds to basic requests.\"\"\"\n        proc, port = gunicorn_server\n\n        response = make_request('127.0.0.1', port)\n        assert b'Hello, World!' in response\n\n    def test_graceful_shutdown_sigterm(self, gunicorn_server):\n        \"\"\"Verify SIGTERM causes graceful shutdown.\"\"\"\n        proc, port = gunicorn_server\n\n        # Verify server is working\n        response = make_request('127.0.0.1', port)\n        assert b'Hello, World!' in response\n\n        # Send SIGTERM to the process group for reliable signal delivery\n        try:\n            os.killpg(os.getpgid(proc.pid), signal.SIGTERM)\n        except (ProcessLookupError, OSError):\n            proc.send_signal(signal.SIGTERM)\n\n        # Wait for process to exit\n        try:\n            exit_code = proc.wait(timeout=CI_TIMEOUT)\n            assert exit_code == 0, f\"Expected exit code 0, got {exit_code}\"\n        except subprocess.TimeoutExpired:\n            proc.kill()\n            pytest.fail(\"Gunicorn did not exit within timeout after SIGTERM\")\n\n    def test_graceful_shutdown_sigint(self, gunicorn_server):\n        \"\"\"Verify SIGINT causes graceful shutdown.\"\"\"\n        proc, port = gunicorn_server\n\n        # Verify server is working\n        response = make_request('127.0.0.1', port)\n        assert b'Hello, World!' in response\n\n        # Send SIGINT to the process group for reliable signal delivery\n        try:\n            os.killpg(os.getpgid(proc.pid), signal.SIGINT)\n        except (ProcessLookupError, OSError):\n            proc.send_signal(signal.SIGINT)\n\n        # Wait for process to exit\n        try:\n            exit_code = proc.wait(timeout=CI_TIMEOUT)\n            assert exit_code == 0, f\"Expected exit code 0, got {exit_code}\"\n        except subprocess.TimeoutExpired:\n            proc.kill()\n            pytest.fail(\"Gunicorn did not exit within timeout after SIGINT\")\n\n    def test_sighup_reload(self, gunicorn_server):\n        \"\"\"Verify SIGHUP triggers reload.\"\"\"\n        proc, port = gunicorn_server\n\n        # Verify server is working\n        response = make_request('127.0.0.1', port)\n        assert b'Hello, World!' in response\n\n        # Send SIGHUP to the master process (not process group - only master handles reload)\n        proc.send_signal(signal.SIGHUP)\n\n        # Wait a moment for reload\n        time.sleep(2)\n\n        # Verify server still works after reload\n        assert proc.poll() is None, \"Server died after SIGHUP\"\n        response = make_request('127.0.0.1', port)\n        assert b'Hello, World!' in response\n\n    def test_multiple_requests_under_load(self, gunicorn_server):\n        \"\"\"Verify server handles multiple concurrent requests.\"\"\"\n        proc, port = gunicorn_server\n\n        # Make several requests in sequence\n        for _ in range(10):\n            response = make_request('127.0.0.1', port)\n            assert b'Hello, World!' in response\n\n        # Verify server is still running\n        assert proc.poll() is None\n\n\nif __name__ == '__main__':\n    pytest.main([__file__, '-v'])\n"
  },
  {
    "path": "tests/test_sock.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom unittest import mock\n\nfrom gunicorn import sock\n\n\n@mock.patch('os.stat')\ndef test_create_sockets_unix_bytes(stat):\n    conf = mock.Mock(address=[b'127.0.0.1:8000'])\n    log = mock.Mock()\n    with mock.patch.object(sock.UnixSocket, '__init__', lambda *args: None):\n        listeners = sock.create_sockets(conf, log)\n        assert len(listeners) == 1\n        print(type(listeners[0]))\n        assert isinstance(listeners[0], sock.UnixSocket)\n\n\n@mock.patch('os.stat')\ndef test_create_sockets_unix_strings(stat):\n    conf = mock.Mock(address=['127.0.0.1:8000'])\n    log = mock.Mock()\n    with mock.patch.object(sock.UnixSocket, '__init__', lambda *args: None):\n        listeners = sock.create_sockets(conf, log)\n        assert len(listeners) == 1\n        assert isinstance(listeners[0], sock.UnixSocket)\n\n\ndef test_socket_close():\n    listener1 = mock.Mock()\n    listener1.getsockname.return_value = ('127.0.0.1', '80')\n    listener2 = mock.Mock()\n    listener2.getsockname.return_value = ('192.168.2.5', '80')\n    sock.close_sockets([listener1, listener2])\n    listener1.close.assert_called_with()\n    listener2.close.assert_called_with()\n\n\n@mock.patch('os.unlink')\ndef test_unix_socket_close_unlink(unlink):\n    listener = mock.Mock()\n    listener.getsockname.return_value = '/var/run/test.sock'\n    sock.close_sockets([listener])\n    listener.close.assert_called_with()\n    unlink.assert_called_once_with('/var/run/test.sock')\n\n\n@mock.patch('os.unlink')\ndef test_unix_socket_close_without_unlink(unlink):\n    listener = mock.Mock()\n    listener.getsockname.return_value = '/var/run/test.sock'\n    sock.close_sockets([listener], False)\n    listener.close.assert_called_with()\n    assert not unlink.called, 'unlink should not have been called'\n"
  },
  {
    "path": "tests/test_ssl.py",
    "content": "# Copyright 2013 Dariusz Suchojad <dsuch at zato.io>\n#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport pytest\n\nfrom gunicorn.config import (\n    KeyFile, CertFile, CACerts, SuppressRaggedEOFs,\n    DoHandshakeOnConnect, Setting, Ciphers,\n)\n\nssl = pytest.importorskip('ssl')\n\n\ndef test_keyfile():\n    assert issubclass(KeyFile, Setting)\n    assert KeyFile.name == 'keyfile'\n    assert KeyFile.section == 'SSL'\n    assert KeyFile.cli == ['--keyfile']\n    assert KeyFile.meta == 'FILE'\n    assert KeyFile.default is None\n\n\ndef test_certfile():\n    assert issubclass(CertFile, Setting)\n    assert CertFile.name == 'certfile'\n    assert CertFile.section == 'SSL'\n    assert CertFile.cli == ['--certfile']\n    assert CertFile.default is None\n\n\ndef test_cacerts():\n    assert issubclass(CACerts, Setting)\n    assert CACerts.name == 'ca_certs'\n    assert CACerts.section == 'SSL'\n    assert CACerts.cli == ['--ca-certs']\n    assert CACerts.meta == 'FILE'\n    assert CACerts.default is None\n\n\ndef test_suppress_ragged_eofs():\n    assert issubclass(SuppressRaggedEOFs, Setting)\n    assert SuppressRaggedEOFs.name == 'suppress_ragged_eofs'\n    assert SuppressRaggedEOFs.section == 'SSL'\n    assert SuppressRaggedEOFs.cli == ['--suppress-ragged-eofs']\n    assert SuppressRaggedEOFs.action == 'store_true'\n    assert SuppressRaggedEOFs.default is True\n\n\ndef test_do_handshake_on_connect():\n    assert issubclass(DoHandshakeOnConnect, Setting)\n    assert DoHandshakeOnConnect.name == 'do_handshake_on_connect'\n    assert DoHandshakeOnConnect.section == 'SSL'\n    assert DoHandshakeOnConnect.cli == ['--do-handshake-on-connect']\n    assert DoHandshakeOnConnect.action == 'store_true'\n    assert DoHandshakeOnConnect.default is False\n\n\ndef test_ciphers():\n    assert issubclass(Ciphers, Setting)\n    assert Ciphers.name == 'ciphers'\n    assert Ciphers.section == 'SSL'\n    assert Ciphers.cli == ['--ciphers']\n    assert Ciphers.default is None\n"
  },
  {
    "path": "tests/test_statsd.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport io\nimport logging\nimport os\nimport shutil\nimport socket\nimport tempfile\nfrom datetime import timedelta\nfrom types import SimpleNamespace\n\nfrom gunicorn.config import Config\nfrom gunicorn.instrument.statsd import Statsd\n\n\nclass StatsdTestException(Exception):\n    pass\n\n\nclass MockSocket:\n    \"Pretend to be a UDP socket\"\n    def __init__(self, failp):\n        self.failp = failp\n        self.msgs = []  # accumulate messages for later inspection\n\n    def send(self, msg):\n        if self.failp:\n            raise StatsdTestException(\"Should not interrupt the logger\")\n\n        sock_dir = tempfile.mkdtemp()\n        sock_file = os.path.join(sock_dir, \"test.sock\")\n\n        server = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)\n        client = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)\n\n        try:\n            server.bind(sock_file)\n            client.connect(sock_file)\n\n            client.send(msg)\n            self.msgs.append(server.recv(1024))\n\n        finally:\n            client.close()\n            server.close()\n            shutil.rmtree(sock_dir)\n\n    def reset(self):\n        self.msgs = []\n\n\ndef test_statsd_fail():\n    \"UDP socket fails\"\n    logger = Statsd(Config())\n    logger.sock = MockSocket(True)\n    logger.info(\"No impact on logging\")\n    logger.debug(\"No impact on logging\")\n    logger.critical(\"No impact on logging\")\n    logger.error(\"No impact on logging\")\n    logger.warning(\"No impact on logging\")\n    logger.exception(\"No impact on logging\")\n\n\ndef test_statsd_host_initialization():\n    c = Config()\n    c.set('statsd_host', 'unix:test.sock')\n    logger = Statsd(c)\n    logger.info(\"Can be initialized and used with a UDS socket\")\n\n    # Can be initialized and used with a UDP address\n    c.set('statsd_host', 'host:8080')\n    logger = Statsd(c)\n    logger.info(\"Can be initialized and used with a UDP socket\")\n\n\ndef test_dogstatsd_tags():\n    c = Config()\n    tags = 'yucatan,libertine:rhubarb'\n    c.set('dogstatsd_tags', tags)\n    logger = Statsd(c)\n    logger.sock = MockSocket(False)\n    logger.info(\"Twill\", extra={\"mtype\": \"gauge\", \"metric\": \"barb.westerly\",\n                                \"value\": 2})\n    assert logger.sock.msgs[0] == b\"barb.westerly:2|g|#\" + tags.encode('ascii')\n\n\ndef test_instrument():\n    logger = Statsd(Config())\n    # Capture logged messages\n    sio = io.StringIO()\n    logger.error_log.addHandler(logging.StreamHandler(sio))\n    logger.sock = MockSocket(False)\n\n    # Regular message\n    logger.info(\"Blah\", extra={\"mtype\": \"gauge\", \"metric\": \"gunicorn.test\", \"value\": 666})\n    assert logger.sock.msgs[0] == b\"gunicorn.test:666|g\"\n    assert sio.getvalue() == \"Blah\\n\"\n    logger.sock.reset()\n\n    # Only metrics, no logging\n    logger.info(\"\", extra={\"mtype\": \"gauge\", \"metric\": \"gunicorn.test\", \"value\": 666})\n    assert logger.sock.msgs[0] == b\"gunicorn.test:666|g\"\n    assert sio.getvalue() == \"Blah\\n\"  # log is unchanged\n    logger.sock.reset()\n\n    # Debug logging also supports metrics\n    logger.debug(\"\", extra={\"mtype\": \"gauge\", \"metric\": \"gunicorn.debug\", \"value\": 667})\n    assert logger.sock.msgs[0] == b\"gunicorn.debug:667|g\"\n    assert sio.getvalue() == \"Blah\\n\"  # log is unchanged\n    logger.sock.reset()\n\n    logger.critical(\"Boom\")\n    assert logger.sock.msgs[0] == b\"gunicorn.log.critical:1|c|@1.0\"\n    logger.sock.reset()\n\n    logger.access(SimpleNamespace(status=\"200 OK\"), None, {}, timedelta(seconds=7))\n    assert logger.sock.msgs[0] == b\"gunicorn.request.duration:7000.0|ms\"\n    assert logger.sock.msgs[1] == b\"gunicorn.requests:1|c|@1.0\"\n    assert logger.sock.msgs[2] == b\"gunicorn.request.status.200:1|c|@1.0\"\n\n\ndef test_prefix():\n    c = Config()\n    c.set(\"statsd_prefix\", \"test.\")\n    logger = Statsd(c)\n    logger.sock = MockSocket(False)\n\n    logger.info(\"Blah\", extra={\"mtype\": \"gauge\", \"metric\": \"gunicorn.test\", \"value\": 666})\n    assert logger.sock.msgs[0] == b\"test.gunicorn.test:666|g\"\n\n\ndef test_prefix_no_dot():\n    c = Config()\n    c.set(\"statsd_prefix\", \"test\")\n    logger = Statsd(c)\n    logger.sock = MockSocket(False)\n\n    logger.info(\"Blah\", extra={\"mtype\": \"gauge\", \"metric\": \"gunicorn.test\", \"value\": 666})\n    assert logger.sock.msgs[0] == b\"test.gunicorn.test:666|g\"\n\n\ndef test_prefix_multiple_dots():\n    c = Config()\n    c.set(\"statsd_prefix\", \"test...\")\n    logger = Statsd(c)\n    logger.sock = MockSocket(False)\n\n    logger.info(\"Blah\", extra={\"mtype\": \"gauge\", \"metric\": \"gunicorn.test\", \"value\": 666})\n    assert logger.sock.msgs[0] == b\"test.gunicorn.test:666|g\"\n\n\ndef test_prefix_nested():\n    c = Config()\n    c.set(\"statsd_prefix\", \"test.asdf.\")\n    logger = Statsd(c)\n    logger.sock = MockSocket(False)\n\n    logger.info(\"Blah\", extra={\"mtype\": \"gauge\", \"metric\": \"gunicorn.test\", \"value\": 666})\n    assert logger.sock.msgs[0] == b\"test.asdf.gunicorn.test:666|g\"\n"
  },
  {
    "path": "tests/test_systemd.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom contextlib import contextmanager\nimport os\nfrom unittest import mock\n\nimport pytest\n\nfrom gunicorn import systemd\n\n\n@contextmanager\ndef check_environ(unset=True):\n    \"\"\"\n    A context manager that asserts post-conditions of ``listen_fds`` at exit.\n\n    This helper is used to ease checking of the test post-conditions for the\n    systemd socket activation tests that parametrize the call argument.\n    \"\"\"\n\n    with mock.patch.dict(os.environ):\n        old_fds = os.environ.get('LISTEN_FDS', None)\n        old_pid = os.environ.get('LISTEN_PID', None)\n\n        yield\n\n        if unset:\n            assert 'LISTEN_FDS' not in os.environ, \\\n                \"LISTEN_FDS should have been unset\"\n            assert 'LISTEN_PID' not in os.environ, \\\n                \"LISTEN_PID should have been unset\"\n        else:\n            new_fds = os.environ.get('LISTEN_FDS', None)\n            new_pid = os.environ.get('LISTEN_PID', None)\n            assert new_fds == old_fds, \\\n                \"LISTEN_FDS should not have been changed\"\n            assert new_pid == old_pid, \\\n                \"LISTEN_PID should not have been changed\"\n\n\n@pytest.mark.parametrize(\"unset\", [True, False])\ndef test_listen_fds_ignores_wrong_pid(unset):\n    with mock.patch.dict(os.environ):\n        os.environ['LISTEN_FDS'] = str(5)\n        os.environ['LISTEN_PID'] = str(1)\n        with check_environ(False):  # early exit — never changes the environment\n            assert systemd.listen_fds(unset) == 0, \\\n                \"should ignore listen fds not intended for this pid\"\n\n\n@pytest.mark.parametrize(\"unset\", [True, False])\ndef test_listen_fds_returns_count(unset):\n    with mock.patch.dict(os.environ):\n        os.environ['LISTEN_FDS'] = str(5)\n        os.environ['LISTEN_PID'] = str(os.getpid())\n        with check_environ(unset):\n            assert systemd.listen_fds(unset) == 5, \\\n                \"should return the correct count of fds\"\n"
  },
  {
    "path": "tests/test_util.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\nimport os\n\nimport pytest\n\nfrom gunicorn import util\nfrom gunicorn.errors import AppImportError\nfrom urllib.parse import SplitResult\n\n\n@pytest.mark.parametrize('test_input, expected', [\n    ('unix://var/run/test.sock', 'var/run/test.sock'),\n    ('unix:/var/run/test.sock', '/var/run/test.sock'),\n    ('tcp://localhost', ('localhost', 8000)),\n    ('tcp://localhost:5000', ('localhost', 5000)),\n    ('', ('0.0.0.0', 8000)),\n    ('[::1]:8000', ('::1', 8000)),\n    ('[::1]:5000', ('::1', 5000)),\n    ('[::1]', ('::1', 8000)),\n    ('localhost:8000', ('localhost', 8000)),\n    ('127.0.0.1:8000', ('127.0.0.1', 8000)),\n    ('localhost', ('localhost', 8000)),\n    ('fd://33', 33),\n])\ndef test_parse_address(test_input, expected):\n    assert util.parse_address(test_input) == expected\n\n\ndef test_parse_address_invalid():\n    with pytest.raises(RuntimeError) as exc_info:\n        util.parse_address('127.0.0.1:test')\n    assert \"'test' is not a valid port number.\" in str(exc_info.value)\n\n\ndef test_parse_fd_invalid():\n    with pytest.raises(RuntimeError) as exc_info:\n        util.parse_address('fd://asd')\n    assert \"'asd' is not a valid file descriptor.\" in str(exc_info.value)\n\n\ndef test_http_date():\n    assert util.http_date(1508607753.740316) == 'Sat, 21 Oct 2017 17:42:33 GMT'\n\n\n@pytest.mark.parametrize('test_input, expected', [\n    ('1200:0000:AB00:1234:0000:2552:7777:1313', True),\n    ('1200::AB00:1234::2552:7777:1313', False),\n    ('21DA:D3:0:2F3B:2AA:FF:FE28:9C5A', True),\n    ('1200:0000:AB00:1234:O000:2552:7777:1313', False),\n])\ndef test_is_ipv6(test_input, expected):\n    assert util.is_ipv6(test_input) == expected\n\n\ndef test_warn(capsys):\n    util.warn('test warn')\n    _, err = capsys.readouterr()\n    assert '!!! WARNING: test warn' in err\n\n\n@pytest.mark.parametrize(\n    \"value\",\n    [\n        \"support\",\n        \"support:app\",\n        \"support:create_app()\",\n        \"support:create_app('Gunicorn', 3)\",\n        \"support:create_app(count=3)\",\n    ],\n)\ndef test_import_app_good(value):\n    assert util.import_app(value)\n\n\n@pytest.mark.parametrize(\n    (\"value\", \"exc_type\", \"msg\"),\n    [\n        (\"a:app\", ImportError, \"No module\"),\n        (\"support:create_app(\", AppImportError, \"Failed to parse\"),\n        (\"support:create.app()\", AppImportError, \"Function reference\"),\n        (\"support:create_app(Gunicorn)\", AppImportError, \"literal values\"),\n        (\"support:create.app\", AppImportError, \"attribute name\"),\n        (\"support:wrong_app\", AppImportError, \"find attribute\"),\n        (\"support:error_factory(1)\", AppImportError, \"error_factory() takes\"),\n        (\"support:error_factory()\", TypeError, \"inner\"),\n        (\"support:none_app\", AppImportError, \"find application object\"),\n        (\"support:HOST\", AppImportError, \"callable\"),\n    ],\n)\ndef test_import_app_bad(value, exc_type, msg):\n    with pytest.raises(exc_type) as exc_info:\n        util.import_app(value)\n\n    assert msg in str(exc_info.value)\n\n\ndef test_import_app_py_ext(monkeypatch):\n    monkeypatch.chdir(os.path.dirname(__file__))\n\n    with pytest.raises(ImportError) as exc_info:\n        util.import_app(\"support.py\")\n\n    assert \"did you mean\" in str(exc_info.value)\n\n\ndef test_to_bytestring():\n    assert util.to_bytestring('test_str', 'ascii') == b'test_str'\n    assert util.to_bytestring('test_str®') == b'test_str\\xc2\\xae'\n    assert util.to_bytestring(b'byte_test_str') == b'byte_test_str'\n    with pytest.raises(TypeError) as exc_info:\n        util.to_bytestring(100)\n    msg = '100 is not a string'\n    assert msg in str(exc_info.value)\n\n\n@pytest.mark.parametrize('test_input, expected', [\n    ('https://example.org/a/b?c=1#d',\n     SplitResult(scheme='https', netloc='example.org', path='/a/b', query='c=1', fragment='d')),\n    ('a/b?c=1#d',\n     SplitResult(scheme='', netloc='', path='a/b', query='c=1', fragment='d')),\n    ('/a/b?c=1#d',\n     SplitResult(scheme='', netloc='', path='/a/b', query='c=1', fragment='d')),\n    ('//a/b?c=1#d',\n     SplitResult(scheme='', netloc='', path='//a/b', query='c=1', fragment='d')),\n    ('///a/b?c=1#d',\n     SplitResult(scheme='', netloc='', path='///a/b', query='c=1', fragment='d')),\n])\ndef test_split_request_uri(test_input, expected):\n    assert util.split_request_uri(test_input) == expected\n"
  },
  {
    "path": "tests/test_uwsgi.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport io\nimport pytest\nfrom unittest import mock\n\nfrom gunicorn.uwsgi import (\n    UWSGIRequest,\n    UWSGIParser,\n    UWSGIParseException,\n    InvalidUWSGIHeader,\n    UnsupportedModifier,\n    ForbiddenUWSGIRequest,\n)\nfrom gunicorn.http.unreader import IterUnreader\n\n\ndef make_uwsgi_packet(vars_dict, modifier1=0, modifier2=0):\n    \"\"\"Create uWSGI packet for testing.\n\n    Args:\n        vars_dict: Dict of WSGI environ variables\n        modifier1: Packet type (0 = WSGI request)\n        modifier2: Additional flags\n\n    Returns:\n        bytes: Complete uWSGI packet\n    \"\"\"\n    vars_data = b''\n    for key, value in vars_dict.items():\n        k = key.encode('latin-1')\n        v = value.encode('latin-1')\n        vars_data += len(k).to_bytes(2, 'little') + k\n        vars_data += len(v).to_bytes(2, 'little') + v\n\n    header = bytes([modifier1]) + len(vars_data).to_bytes(2, 'little') + bytes([modifier2])\n    return header + vars_data\n\n\ndef make_uwsgi_packet_with_body(vars_dict, body=b'', modifier1=0, modifier2=0):\n    \"\"\"Create uWSGI packet with body for testing.\"\"\"\n    if body:\n        vars_dict = dict(vars_dict)\n        vars_dict['CONTENT_LENGTH'] = str(len(body))\n    return make_uwsgi_packet(vars_dict, modifier1, modifier2) + body\n\n\nclass MockConfig:\n    \"\"\"Mock config object for testing.\"\"\"\n\n    def __init__(self, is_ssl=False, uwsgi_allow_ips=None):\n        self.is_ssl = is_ssl\n        self.uwsgi_allow_ips = uwsgi_allow_ips or ['127.0.0.1', '::1']\n\n\nclass TestUWSGIPacketConstruction:\n    \"\"\"Test the packet construction helper.\"\"\"\n\n    def test_empty_vars(self):\n        packet = make_uwsgi_packet({})\n        assert packet == b'\\x00\\x00\\x00\\x00'  # modifier1=0, size=0, modifier2=0\n\n    def test_single_var(self):\n        packet = make_uwsgi_packet({'KEY': 'val'})\n        # Header: modifier1(0) + size(10 in LE) + modifier2(0)\n        # Var: key_size(3 in LE) + 'KEY' + val_size(3 in LE) + 'val'\n        # Size = 2 + 3 + 2 + 3 = 10 bytes\n        expected_header = b'\\x00\\x0a\\x00\\x00'\n        expected_var = b'\\x03\\x00KEY\\x03\\x00val'\n        assert packet == expected_header + expected_var\n\n    def test_multiple_vars(self):\n        packet = make_uwsgi_packet({'A': '1', 'B': '2'})\n        assert len(packet) == 4 + (2 + 1 + 2 + 1) * 2  # header + 2 vars\n\n\nclass TestUWSGIRequest:\n    \"\"\"Test UWSGIRequest parsing.\"\"\"\n\n    def test_parse_simple_request(self):\n        \"\"\"Test parsing a simple GET request.\"\"\"\n        packet = make_uwsgi_packet({\n            'REQUEST_METHOD': 'GET',\n            'PATH_INFO': '/test',\n            'QUERY_STRING': 'foo=bar',\n        })\n        unreader = IterUnreader([packet])\n        cfg = MockConfig()\n\n        req = UWSGIRequest(cfg, unreader, ('127.0.0.1', 12345))\n\n        assert req.method == 'GET'\n        assert req.path == '/test'\n        assert req.query == 'foo=bar'\n        assert req.uri == '/test?foo=bar'\n\n    def test_parse_post_request_with_body(self):\n        \"\"\"Test parsing a POST request with body.\"\"\"\n        body = b'name=test&value=123'\n        packet = make_uwsgi_packet_with_body({\n            'REQUEST_METHOD': 'POST',\n            'PATH_INFO': '/submit',\n            'CONTENT_TYPE': 'application/x-www-form-urlencoded',\n        }, body)\n        unreader = IterUnreader([packet])\n        cfg = MockConfig()\n\n        req = UWSGIRequest(cfg, unreader, ('127.0.0.1', 12345))\n\n        assert req.method == 'POST'\n        assert req.path == '/submit'\n        assert req.body.read() == body\n\n    def test_parse_headers(self):\n        \"\"\"Test that HTTP_* vars become headers.\"\"\"\n        packet = make_uwsgi_packet({\n            'REQUEST_METHOD': 'GET',\n            'PATH_INFO': '/',\n            'HTTP_HOST': 'example.com',\n            'HTTP_USER_AGENT': 'TestClient/1.0',\n            'HTTP_ACCEPT': 'text/html',\n        })\n        unreader = IterUnreader([packet])\n        cfg = MockConfig()\n\n        req = UWSGIRequest(cfg, unreader, ('127.0.0.1', 12345))\n\n        headers_dict = dict(req.headers)\n        assert headers_dict['HOST'] == 'example.com'\n        assert headers_dict['USER-AGENT'] == 'TestClient/1.0'\n        assert headers_dict['ACCEPT'] == 'text/html'\n\n    def test_parse_content_type_header(self):\n        \"\"\"Test that CONTENT_TYPE becomes a header.\"\"\"\n        packet = make_uwsgi_packet({\n            'REQUEST_METHOD': 'POST',\n            'PATH_INFO': '/',\n            'CONTENT_TYPE': 'application/json',\n            'CONTENT_LENGTH': '0',\n        })\n        unreader = IterUnreader([packet])\n        cfg = MockConfig()\n\n        req = UWSGIRequest(cfg, unreader, ('127.0.0.1', 12345))\n\n        headers_dict = dict(req.headers)\n        assert headers_dict['CONTENT-TYPE'] == 'application/json'\n        assert headers_dict['CONTENT-LENGTH'] == '0'\n\n    def test_https_scheme(self):\n        \"\"\"Test scheme detection from HTTPS variable.\"\"\"\n        packet = make_uwsgi_packet({\n            'REQUEST_METHOD': 'GET',\n            'PATH_INFO': '/',\n            'HTTPS': 'on',\n        })\n        unreader = IterUnreader([packet])\n        cfg = MockConfig()\n\n        req = UWSGIRequest(cfg, unreader, ('127.0.0.1', 12345))\n\n        assert req.scheme == 'https'\n\n    def test_wsgi_url_scheme(self):\n        \"\"\"Test scheme from wsgi.url_scheme variable.\"\"\"\n        packet = make_uwsgi_packet({\n            'REQUEST_METHOD': 'GET',\n            'PATH_INFO': '/',\n            'wsgi.url_scheme': 'https',\n        })\n        unreader = IterUnreader([packet])\n        cfg = MockConfig()\n\n        req = UWSGIRequest(cfg, unreader, ('127.0.0.1', 12345))\n\n        assert req.scheme == 'https'\n\n    def test_default_values(self):\n        \"\"\"Test default values when vars are missing.\"\"\"\n        packet = make_uwsgi_packet({})\n        unreader = IterUnreader([packet])\n        cfg = MockConfig()\n\n        req = UWSGIRequest(cfg, unreader, ('127.0.0.1', 12345))\n\n        assert req.method == 'GET'\n        assert req.path == '/'\n        assert req.query == ''\n        assert req.uri == '/'\n\n    def test_uwsgi_vars_preserved(self):\n        \"\"\"Test that all vars are preserved in uwsgi_vars.\"\"\"\n        packet = make_uwsgi_packet({\n            'REQUEST_METHOD': 'GET',\n            'PATH_INFO': '/',\n            'SERVER_NAME': 'localhost',\n            'SERVER_PORT': '8000',\n            'CUSTOM_VAR': 'custom_value',\n        })\n        unreader = IterUnreader([packet])\n        cfg = MockConfig()\n\n        req = UWSGIRequest(cfg, unreader, ('127.0.0.1', 12345))\n\n        assert req.uwsgi_vars['SERVER_NAME'] == 'localhost'\n        assert req.uwsgi_vars['SERVER_PORT'] == '8000'\n        assert req.uwsgi_vars['CUSTOM_VAR'] == 'custom_value'\n\n\nclass TestUWSGIRequestErrors:\n    \"\"\"Test UWSGIRequest error handling.\"\"\"\n\n    def test_incomplete_header(self):\n        \"\"\"Test error on incomplete header.\"\"\"\n        unreader = IterUnreader([b'\\x00\\x00'])  # Only 2 bytes\n        cfg = MockConfig()\n\n        with pytest.raises(InvalidUWSGIHeader) as exc_info:\n            UWSGIRequest(cfg, unreader, ('127.0.0.1', 12345))\n        assert 'incomplete header' in str(exc_info.value)\n\n    def test_incomplete_vars_block(self):\n        \"\"\"Test error on truncated vars block.\"\"\"\n        # Header says 100 bytes of vars, but we only provide 10\n        header = b'\\x00\\x64\\x00\\x00'  # modifier1=0, size=100, modifier2=0\n        unreader = IterUnreader([header + b'1234567890'])\n        cfg = MockConfig()\n\n        with pytest.raises(InvalidUWSGIHeader) as exc_info:\n            UWSGIRequest(cfg, unreader, ('127.0.0.1', 12345))\n        assert 'incomplete vars block' in str(exc_info.value)\n\n    def test_unsupported_modifier(self):\n        \"\"\"Test error on non-zero modifier1.\"\"\"\n        packet = bytes([1]) + b'\\x00\\x00\\x00'  # modifier1=1\n        unreader = IterUnreader([packet])\n        cfg = MockConfig()\n\n        with pytest.raises(UnsupportedModifier) as exc_info:\n            UWSGIRequest(cfg, unreader, ('127.0.0.1', 12345))\n        assert exc_info.value.modifier == 1\n        assert exc_info.value.code == 501\n\n    def test_truncated_key_size(self):\n        \"\"\"Test error on truncated key size.\"\"\"\n        header = b'\\x00\\x01\\x00\\x00'  # size=1, but need at least 2 bytes for key_size\n        unreader = IterUnreader([header + b'X'])\n        cfg = MockConfig()\n\n        with pytest.raises(InvalidUWSGIHeader) as exc_info:\n            UWSGIRequest(cfg, unreader, ('127.0.0.1', 12345))\n        assert 'truncated' in str(exc_info.value)\n\n    def test_forbidden_ip(self):\n        \"\"\"Test error when source IP not in allow list.\"\"\"\n        packet = make_uwsgi_packet({'REQUEST_METHOD': 'GET', 'PATH_INFO': '/'})\n        unreader = IterUnreader([packet])\n        cfg = MockConfig(uwsgi_allow_ips=['192.168.1.1'])\n\n        with pytest.raises(ForbiddenUWSGIRequest) as exc_info:\n            UWSGIRequest(cfg, unreader, ('10.0.0.1', 12345))\n        assert exc_info.value.code == 403\n        assert '10.0.0.1' in str(exc_info.value)\n\n    def test_allowed_ip_wildcard(self):\n        \"\"\"Test that wildcard allows any IP.\"\"\"\n        packet = make_uwsgi_packet({'REQUEST_METHOD': 'GET', 'PATH_INFO': '/'})\n        unreader = IterUnreader([packet])\n        cfg = MockConfig(uwsgi_allow_ips=['*'])\n\n        # Should not raise\n        req = UWSGIRequest(cfg, unreader, ('10.0.0.1', 12345))\n        assert req.method == 'GET'\n\n    def test_unix_socket_always_allowed(self):\n        \"\"\"Test that UNIX socket connections are always allowed.\"\"\"\n        packet = make_uwsgi_packet({'REQUEST_METHOD': 'GET', 'PATH_INFO': '/'})\n        unreader = IterUnreader([packet])\n        cfg = MockConfig(uwsgi_allow_ips=['127.0.0.1'])\n\n        # UNIX socket has non-tuple peer_addr\n        req = UWSGIRequest(cfg, unreader, None)\n        assert req.method == 'GET'\n\n\nclass TestUWSGIRequestConnection:\n    \"\"\"Test connection handling.\"\"\"\n\n    def test_should_close_default(self):\n        \"\"\"Test default keep-alive behavior.\"\"\"\n        packet = make_uwsgi_packet({'REQUEST_METHOD': 'GET', 'PATH_INFO': '/'})\n        unreader = IterUnreader([packet])\n        cfg = MockConfig()\n\n        req = UWSGIRequest(cfg, unreader, ('127.0.0.1', 12345))\n\n        assert req.should_close() is False\n\n    def test_should_close_connection_close(self):\n        \"\"\"Test Connection: close header.\"\"\"\n        packet = make_uwsgi_packet({\n            'REQUEST_METHOD': 'GET',\n            'PATH_INFO': '/',\n            'HTTP_CONNECTION': 'close',\n        })\n        unreader = IterUnreader([packet])\n        cfg = MockConfig()\n\n        req = UWSGIRequest(cfg, unreader, ('127.0.0.1', 12345))\n\n        assert req.should_close() is True\n\n    def test_should_close_connection_keepalive(self):\n        \"\"\"Test Connection: keep-alive header.\"\"\"\n        packet = make_uwsgi_packet({\n            'REQUEST_METHOD': 'GET',\n            'PATH_INFO': '/',\n            'HTTP_CONNECTION': 'keep-alive',\n        })\n        unreader = IterUnreader([packet])\n        cfg = MockConfig()\n\n        req = UWSGIRequest(cfg, unreader, ('127.0.0.1', 12345))\n\n        assert req.should_close() is False\n\n    def test_force_close(self):\n        \"\"\"Test force_close method.\"\"\"\n        packet = make_uwsgi_packet({'REQUEST_METHOD': 'GET', 'PATH_INFO': '/'})\n        unreader = IterUnreader([packet])\n        cfg = MockConfig()\n\n        req = UWSGIRequest(cfg, unreader, ('127.0.0.1', 12345))\n        req.force_close()\n\n        assert req.should_close() is True\n\n\nclass TestUWSGIParser:\n    \"\"\"Test UWSGIParser.\"\"\"\n\n    def test_parser_iteration(self):\n        \"\"\"Test iterating over parser for multiple requests.\"\"\"\n        packet = make_uwsgi_packet({\n            'REQUEST_METHOD': 'GET',\n            'PATH_INFO': '/test',\n            'HTTP_CONNECTION': 'close',  # Single request\n        })\n        cfg = MockConfig()\n\n        # Parser expects an iterable source, not an unreader\n        parser = UWSGIParser(cfg, [packet], ('127.0.0.1', 12345))\n        req = next(parser)\n\n        assert req.method == 'GET'\n        assert req.path == '/test'\n\n    def test_parser_mesg_class(self):\n        \"\"\"Test that parser uses UWSGIRequest.\"\"\"\n        assert UWSGIParser.mesg_class is UWSGIRequest\n\n\nclass TestExceptionStrings:\n    \"\"\"Test exception string representations.\"\"\"\n\n    def test_invalid_uwsgi_header_str(self):\n        exc = InvalidUWSGIHeader(\"test message\")\n        assert str(exc) == \"Invalid uWSGI header: test message\"\n        assert exc.code == 400\n\n    def test_unsupported_modifier_str(self):\n        exc = UnsupportedModifier(5)\n        assert str(exc) == \"Unsupported uWSGI modifier1: 5\"\n        assert exc.code == 501\n\n    def test_forbidden_uwsgi_request_str(self):\n        exc = ForbiddenUWSGIRequest(\"10.0.0.1\")\n        assert str(exc) == \"uWSGI request from '10.0.0.1' not allowed\"\n        assert exc.code == 403\n\n\nclass TestUWSGIBody:\n    \"\"\"Test body reading.\"\"\"\n\n    def test_read_body_in_chunks(self):\n        \"\"\"Test reading body in multiple chunks.\"\"\"\n        body = b'A' * 1000\n        packet = make_uwsgi_packet_with_body({\n            'REQUEST_METHOD': 'POST',\n            'PATH_INFO': '/',\n        }, body)\n        unreader = IterUnreader([packet])\n        cfg = MockConfig()\n\n        req = UWSGIRequest(cfg, unreader, ('127.0.0.1', 12345))\n\n        result = b''\n        chunk = req.body.read(100)\n        while chunk:\n            result += chunk\n            chunk = req.body.read(100)\n\n        assert result == body\n\n    def test_invalid_content_length(self):\n        \"\"\"Test handling of invalid CONTENT_LENGTH.\"\"\"\n        packet = make_uwsgi_packet({\n            'REQUEST_METHOD': 'POST',\n            'PATH_INFO': '/',\n            'CONTENT_LENGTH': 'invalid',\n        })\n        unreader = IterUnreader([packet])\n        cfg = MockConfig()\n\n        req = UWSGIRequest(cfg, unreader, ('127.0.0.1', 12345))\n\n        # Invalid content length should default to 0\n        assert req.body.read() == b''\n\n    def test_negative_content_length(self):\n        \"\"\"Test handling of negative CONTENT_LENGTH.\"\"\"\n        packet = make_uwsgi_packet({\n            'REQUEST_METHOD': 'POST',\n            'PATH_INFO': '/',\n            'CONTENT_LENGTH': '-5',\n        })\n        unreader = IterUnreader([packet])\n        cfg = MockConfig()\n\n        req = UWSGIRequest(cfg, unreader, ('127.0.0.1', 12345))\n\n        # Negative content length should default to 0\n        assert req.body.read() == b''\n"
  },
  {
    "path": "tests/test_valid_requests.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport glob\nimport os\n\nimport pytest\n\nimport treq\n\ndirname = os.path.dirname(__file__)\nreqdir = os.path.join(dirname, \"requests\", \"valid\")\nhttpfiles = glob.glob(os.path.join(reqdir, \"*.http\"))\n\n\n@pytest.mark.parametrize(\"fname\", httpfiles)\ndef test_http_parser(fname):\n    env = treq.load_py(os.path.splitext(fname)[0] + \".py\")\n\n    expect = env['request']\n    cfg = env['cfg']\n    req = treq.request(fname, expect)\n\n    for case in req.gen_cases(cfg):\n        case[0](*case[1:])\n"
  },
  {
    "path": "tests/treq.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n# Copyright 2009 Paul J. Davis <paul.joseph.davis@gmail.com>\n#\n# This file is part of the pywebmachine package released\n# under the MIT license.\n\nimport inspect\nimport importlib.machinery\nimport os\nimport random\nimport types\n\nfrom gunicorn.config import Config\nfrom gunicorn.http.parser import RequestParser\nfrom gunicorn.util import split_request_uri\n\ndirname = os.path.dirname(__file__)\nrandom.seed()\n\n\ndef uri(data):\n    ret = {\"raw\": data}\n    parts = split_request_uri(data)\n    ret[\"scheme\"] = parts.scheme or ''\n    ret[\"host\"] = parts.netloc.rsplit(\":\", 1)[0] or None\n    ret[\"port\"] = parts.port or 80\n    ret[\"path\"] = parts.path or ''\n    ret[\"query\"] = parts.query or ''\n    ret[\"fragment\"] = parts.fragment or ''\n    return ret\n\n\ndef load_py(fname):\n    module_name = '__config__'\n    mod = types.ModuleType(module_name)\n    setattr(mod, 'uri', uri)\n    setattr(mod, 'cfg', Config())\n    loader = importlib.machinery.SourceFileLoader(module_name, fname)\n    loader.exec_module(mod)\n    return vars(mod)\n\n\ndef decode_hex_escapes(data):\n    \"\"\"Decode hex escape sequences like \\\\xAB in test data.\"\"\"\n    import re\n    result = bytearray()\n    i = 0\n    while i < len(data):\n        # Check for \\xHH hex escape\n        if i + 3 < len(data) and data[i:i+2] == b'\\\\x':\n            hex_chars = data[i+2:i+4]\n            try:\n                byte_val = int(hex_chars, 16)\n                result.append(byte_val)\n                i += 4\n                continue\n            except ValueError:\n                pass\n        result.append(data[i])\n        i += 1\n    return bytes(result)\n\n\nclass request:\n    def __init__(self, fname, expect):\n        self.fname = fname\n        self.name = os.path.basename(fname)\n\n        self.expect = expect\n        if not isinstance(self.expect, list):\n            self.expect = [self.expect]\n\n        with open(self.fname, 'rb') as handle:\n            self.data = handle.read()\n        self.data = self.data.replace(b\"\\n\", b\"\").replace(b\"\\\\r\\\\n\", b\"\\r\\n\")\n        self.data = self.data.replace(b\"\\\\0\", b\"\\000\").replace(b\"\\\\n\", b\"\\n\").replace(b\"\\\\t\", b\"\\t\")\n        # Handle hex escape sequences for binary data (e.g., \\x0D for PROXY v2)\n        self.data = decode_hex_escapes(self.data)\n        if b\"\\\\\" in self.data:\n            raise AssertionError(\"Unexpected backslash in test data - only handling HTAB, NUL, CRLF, and hex escapes\")\n\n    # Functions for sending data to the parser.\n    # These functions mock out reading from a\n    # socket or other data source that might\n    # be used in real life.\n\n    def send_all(self):\n        yield self.data\n\n    def send_lines(self):\n        lines = self.data\n        pos = lines.find(b\"\\r\\n\")\n        while pos > 0:\n            yield lines[:pos+2]\n            lines = lines[pos+2:]\n            pos = lines.find(b\"\\r\\n\")\n        if lines:\n            yield lines\n\n    def send_bytes(self):\n        for d in self.data:\n            yield bytes([d])\n\n    def send_random(self):\n        maxs = round(len(self.data) / 10)\n        read = 0\n        while read < len(self.data):\n            chunk = random.randint(1, maxs)\n            yield self.data[read:read+chunk]\n            read += chunk\n\n    def send_special_chunks(self):\n        \"\"\"Meant to test the request line length check.\n\n        Sends the request data in two chunks, one having a\n        length of 1 byte, which ensures that no CRLF is included,\n        and a second chunk containing the rest of the request data.\n\n        If the request line length check is not done properly,\n        testing the ``tests/requests/valid/099.http`` request\n        fails with a ``LimitRequestLine`` exception.\n\n        \"\"\"\n        chunk = self.data[:1]\n        read = 0\n        while read < len(self.data):\n            yield self.data[read:read+len(chunk)]\n            read += len(chunk)\n            chunk = self.data[read:]\n\n    # These functions define the sizes that the\n    # read functions will read with.\n\n    def size_all(self):\n        return -1\n\n    def size_bytes(self):\n        return 1\n\n    def size_small_random(self):\n        return random.randint(1, 4)\n\n    def size_random(self):\n        return random.randint(1, 4096)\n\n    # Match a body against various ways of reading\n    # a message. Pass in the request, expected body\n    # and one of the size functions.\n\n    def szread(self, func, sizes):\n        sz = sizes()\n        data = func(sz)\n        if 0 <= sz < len(data):\n            raise AssertionError(\"Read more than %d bytes: %s\" % (sz, data))\n        return data\n\n    def match_read(self, req, body, sizes):\n        data = self.szread(req.body.read, sizes)\n        count = 1000\n        while body:\n            if body[:len(data)] != data:\n                raise AssertionError(\"Invalid body data read: %r != %r\" % (\n                                        data, body[:len(data)]))\n            body = body[len(data):]\n            data = self.szread(req.body.read, sizes)\n            if not data:\n                count -= 1\n            if count <= 0:\n                raise AssertionError(\"Unexpected apparent EOF\")\n\n        if body:\n            raise AssertionError(\"Failed to read entire body: %r\" % body)\n        elif data:\n            raise AssertionError(\"Read beyond expected body: %r\" % data)\n        data = req.body.read(sizes())\n        if data:\n            raise AssertionError(\"Read after body finished: %r\" % data)\n\n    def match_readline(self, req, body, sizes):\n        data = self.szread(req.body.readline, sizes)\n        count = 1000\n        while body:\n            if body[:len(data)] != data:\n                raise AssertionError(\"Invalid data read: %r\" % data)\n            if b'\\n' in data[:-1]:\n                raise AssertionError(\"Embedded new line: %r\" % data)\n            body = body[len(data):]\n            data = self.szread(req.body.readline, sizes)\n            if not data:\n                count -= 1\n            if count <= 0:\n                raise AssertionError(\"Apparent unexpected EOF\")\n        if body:\n            raise AssertionError(\"Failed to read entire body: %r\" % body)\n        elif data:\n            raise AssertionError(\"Read beyond expected body: %r\" % data)\n        data = req.body.readline(sizes())\n        if data:\n            raise AssertionError(\"Read data after body finished: %r\" % data)\n\n    def match_readlines(self, req, body, sizes):\n        \"\"\"\\\n        This skips the sizes checks as we don't implement it.\n        \"\"\"\n        data = req.body.readlines()\n        for line in data:\n            if b'\\n' in line[:-1]:\n                raise AssertionError(\"Embedded new line: %r\" % line)\n            if line != body[:len(line)]:\n                raise AssertionError(\"Invalid body data read: %r != %r\" % (\n                                                    line, body[:len(line)]))\n            body = body[len(line):]\n        if body:\n            raise AssertionError(\"Failed to read entire body: %r\" % body)\n        data = req.body.readlines(sizes())\n        if data:\n            raise AssertionError(\"Read data after body finished: %r\" % data)\n\n    def match_iter(self, req, body, sizes):\n        \"\"\"\\\n        This skips sizes because there's its not part of the iter api.\n        \"\"\"\n        for line in req.body:\n            if b'\\n' in line[:-1]:\n                raise AssertionError(\"Embedded new line: %r\" % line)\n            if line != body[:len(line)]:\n                raise AssertionError(\"Invalid body data read: %r != %r\" % (\n                                                    line, body[:len(line)]))\n            body = body[len(line):]\n        if body:\n            raise AssertionError(\"Failed to read entire body: %r\" % body)\n        try:\n            data = next(iter(req.body))\n            raise AssertionError(\"Read data after body finished: %r\" % data)\n        except StopIteration:\n            pass\n\n    # Construct a series of test cases from the permutations of\n    # send, size, and match functions.\n\n    def gen_cases(self, cfg):\n        def get_funs(p):\n            return [v for k, v in inspect.getmembers(self) if k.startswith(p)]\n        senders = get_funs(\"send_\")\n        sizers = get_funs(\"size_\")\n        matchers = get_funs(\"match_\")\n        cfgs = [\n            (mt, sz, sn)\n            for mt in matchers\n            for sz in sizers\n            for sn in senders\n        ]\n\n        ret = []\n        for (mt, sz, sn) in cfgs:\n            if hasattr(mt, 'funcname'):\n                mtn = mt.func_name[6:]\n                szn = sz.func_name[5:]\n                snn = sn.func_name[5:]\n            else:\n                mtn = mt.__name__[6:]\n                szn = sz.__name__[5:]\n                snn = sn.__name__[5:]\n\n            def test_req(sn, sz, mt):\n                self.check(cfg, sn, sz, mt)\n            desc = \"%s: MT: %s SZ: %s SN: %s\" % (self.name, mtn, szn, snn)\n            test_req.description = desc\n            ret.append((test_req, sn, sz, mt))\n        return ret\n\n    def check(self, cfg, sender, sizer, matcher):\n        cases = self.expect[:]\n        p = RequestParser(cfg, sender(), None)\n        parsed_request_idx = -1\n        for parsed_request_idx, req in enumerate(p):\n            self.same(req, sizer, matcher, cases.pop(0))\n        assert len(self.expect) == parsed_request_idx + 1\n        assert not cases\n\n    def same(self, req, sizer, matcher, exp):\n        assert req.method == exp[\"method\"]\n        assert req.uri == exp[\"uri\"][\"raw\"]\n        assert req.path == exp[\"uri\"][\"path\"]\n        assert req.query == exp[\"uri\"][\"query\"]\n        assert req.fragment == exp[\"uri\"][\"fragment\"]\n        assert req.version == exp[\"version\"]\n        assert req.headers == exp[\"headers\"]\n        matcher(req, exp[\"body\"], sizer)\n        assert req.trailers == exp.get(\"trailers\", [])\n\n\nclass badrequest:\n    # FIXME: no good reason why this cannot match what the more extensive mechanism above\n    def __init__(self, fname):\n        self.fname = fname\n        self.name = os.path.basename(fname)\n\n        with open(self.fname) as handle:\n            self.data = handle.read()\n        self.data = self.data.replace(\"\\n\", \"\").replace(\"\\\\r\\\\n\", \"\\r\\n\")\n        self.data = self.data.replace(\"\\\\0\", \"\\000\").replace(\"\\\\n\", \"\\n\").replace(\"\\\\t\", \"\\t\")\n        if \"\\\\\" in self.data:\n            raise AssertionError(\"Unexpected backslash in test data - only handling HTAB, NUL and CRLF\")\n        self.data = self.data.encode('latin1')\n\n    def send(self):\n        maxs = round(len(self.data) / 10)\n        read = 0\n        while read < len(self.data):\n            chunk = random.randint(1, maxs)\n            yield self.data[read:read+chunk]\n            read += chunk\n\n    def check(self, cfg):\n        p = RequestParser(cfg, self.send(), None)\n        # must fully consume iterator, otherwise EOF errors could go unnoticed\n        for _ in p:\n            pass\n"
  },
  {
    "path": "tests/workers/__init__.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n"
  },
  {
    "path": "tests/workers/test_gevent_import_order.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\n\"\"\"\nTest for gevent worker compatibility with concurrent.futures import order.\n\nIssue: https://github.com/benoitc/gunicorn/issues/3482\nDiscussion: https://github.com/benoitc/gunicorn/discussions/3481\nGist: https://gist.github.com/markjm/9f724364619c519892e8111fe6520ca6\n\nWhen using gevent workers, `concurrent.futures` must not be imported before\n`gevent.monkey.patch_all()` is called. If it is, certain thread locks in\nconcurrent.futures will not be properly patched, leading to issues with\nlibraries like boto3 that use concurrent.futures internally.\n\nIn gunicorn v25, the import of gunicorn.arbiter triggered the import of\ngunicorn.dirty, which imports concurrent.futures via asyncio. This happened\nbefore user code (like a config file with monkey.patch_all()) could run.\n\nThe fix was to make the dirty module imports lazy - only importing when\ndirty workers are actually being started (in spawn_dirty_arbiter()).\n\"\"\"\n\nimport subprocess\nimport sys\nimport textwrap\n\nimport pytest\n\ntry:\n    import gevent\n    HAS_GEVENT = True\nexcept ImportError:\n    HAS_GEVENT = False\n\npytestmark = pytest.mark.skipif(not HAS_GEVENT, reason=\"gevent not installed\")\n\n\nclass TestConcurrentFuturesImportOrder:\n    \"\"\"Test that concurrent.futures import timing doesn't break gevent patching.\"\"\"\n\n    def test_concurrent_futures_not_imported_by_arbiter(self):\n        \"\"\"Test that importing gunicorn.arbiter does NOT import concurrent.futures.\n\n        The dirty module (which uses asyncio and concurrent.futures) is now\n        imported lazily to avoid breaking gevent patching.\n        See: https://github.com/benoitc/gunicorn/discussions/3481\n        \"\"\"\n        # Run in a subprocess to ensure clean import state\n        code = textwrap.dedent(\"\"\"\n            import sys\n\n            # Verify concurrent.futures is not imported yet\n            assert 'concurrent.futures' not in sys.modules, \\\n                \"concurrent.futures should not be imported yet\"\n\n            # Import gunicorn.arbiter\n            import gunicorn.arbiter\n\n            # Check if concurrent.futures is now imported\n            cf_imported = 'concurrent.futures' in sys.modules\n            print(f\"RESULT:concurrent_futures_imported={cf_imported}\")\n        \"\"\")\n\n        result = subprocess.run(\n            [sys.executable, \"-c\", code],\n            capture_output=True,\n            text=True\n        )\n\n        # Parse the result\n        stdout = result.stdout.strip()\n        assert \"RESULT:concurrent_futures_imported=\" in stdout, \\\n            f\"Test script failed: stderr={result.stderr}\"\n\n        imported = stdout.split(\"RESULT:concurrent_futures_imported=\")[1] == \"True\"\n\n        # concurrent.futures should NOT be imported by gunicorn.arbiter\n        # The dirty module is now imported lazily\n        assert not imported, (\n            \"concurrent.futures should NOT be imported when gunicorn.arbiter is imported. \"\n            \"The dirty module should be imported lazily.\"\n        )\n\n    def test_gevent_patch_after_concurrent_futures_import_leaves_unpatched_lock(self):\n        \"\"\"Test that patching after concurrent.futures import leaves locks unpatched.\n\n        This reproduces the issue from the gist where the _global_shutdown_lock\n        in concurrent.futures.thread is not properly patched if concurrent.futures\n        is imported before monkey.patch_all().\n        \"\"\"\n        # Run in a subprocess to ensure clean import state\n        code = textwrap.dedent(\"\"\"\n            import sys\n\n            # Simulate what happens with gunicorn v25:\n            # concurrent.futures is imported BEFORE gevent patching\n            import concurrent.futures\n            from concurrent.futures import thread as futures_thread\n\n            # Get a reference to the lock BEFORE patching\n            lock_before_patch = futures_thread._global_shutdown_lock\n\n            # Now apply gevent patching (simulating user's config file)\n            from gevent import monkey\n            monkey.patch_all()\n\n            # Get the lock type AFTER patching\n            from gevent.thread import LockType as GeventLockType\n\n            # Check if the lock is a gevent lock\n            is_gevent_lock = isinstance(lock_before_patch, GeventLockType)\n            lock_type = type(lock_before_patch).__module__\n\n            print(f\"RESULT:is_gevent_lock={is_gevent_lock}\")\n            print(f\"RESULT:lock_module={lock_type}\")\n        \"\"\")\n\n        result = subprocess.run(\n            [sys.executable, \"-c\", code],\n            capture_output=True,\n            text=True\n        )\n\n        stdout = result.stdout.strip()\n        assert \"RESULT:is_gevent_lock=\" in stdout, \\\n            f\"Test script failed: stderr={result.stderr}\"\n\n        # Parse results\n        lines = stdout.split(\"\\n\")\n        is_gevent_lock = None\n        lock_module = None\n        for line in lines:\n            if line.startswith(\"RESULT:is_gevent_lock=\"):\n                is_gevent_lock = line.split(\"=\")[1] == \"True\"\n            elif line.startswith(\"RESULT:lock_module=\"):\n                lock_module = line.split(\"=\")[1]\n\n        # Document: when concurrent.futures is imported before patching,\n        # the _global_shutdown_lock is NOT a gevent lock - this is the bug\n        assert is_gevent_lock is False, (\n            \"Lock should NOT be a gevent lock when concurrent.futures \"\n            \"was imported before patching. If this fails, gevent may have \"\n            \"improved their patching.\"\n        )\n        assert lock_module == \"_thread\", (\n            f\"Lock module should be _thread (unpatched), got {lock_module}\"\n        )\n\n    def test_gevent_patch_before_concurrent_futures_import_patches_lock(self):\n        \"\"\"Test that patching BEFORE concurrent.futures import works correctly.\n\n        This shows the correct behavior: when monkey.patch_all() is called\n        BEFORE importing concurrent.futures, the locks are properly patched.\n        \"\"\"\n        # Run in a subprocess to ensure clean import state\n        code = textwrap.dedent(\"\"\"\n            import sys\n\n            # Apply gevent patching FIRST (correct order)\n            from gevent import monkey\n            monkey.patch_all()\n\n            # Now import concurrent.futures\n            import concurrent.futures\n            from concurrent.futures import thread as futures_thread\n\n            # Get a reference to the lock\n            lock = futures_thread._global_shutdown_lock\n\n            # Check if the lock is a gevent lock\n            from gevent.thread import LockType as GeventLockType\n            is_gevent_lock = isinstance(lock, GeventLockType)\n            lock_type = type(lock).__module__\n\n            print(f\"RESULT:is_gevent_lock={is_gevent_lock}\")\n            print(f\"RESULT:lock_module={lock_type}\")\n        \"\"\")\n\n        result = subprocess.run(\n            [sys.executable, \"-c\", code],\n            capture_output=True,\n            text=True\n        )\n\n        stdout = result.stdout.strip()\n        assert \"RESULT:is_gevent_lock=\" in stdout, \\\n            f\"Test script failed: stderr={result.stderr}\"\n\n        # Parse results\n        lines = stdout.split(\"\\n\")\n        is_gevent_lock = None\n        lock_module = None\n        for line in lines:\n            if line.startswith(\"RESULT:is_gevent_lock=\"):\n                is_gevent_lock = line.split(\"=\")[1] == \"True\"\n            elif line.startswith(\"RESULT:lock_module=\"):\n                lock_module = line.split(\"=\")[1]\n\n        # When patching happens BEFORE import, locks are properly patched\n        assert is_gevent_lock is True, (\n            \"Lock should be a gevent lock when patching happens before import\"\n        )\n        assert lock_module == \"gevent.thread\", (\n            f\"Lock module should be gevent.thread, got {lock_module}\"\n        )\n\n    def test_gunicorn_gevent_worker_patching_works(self):\n        \"\"\"Integration test verifying gevent patching works with gunicorn.\n\n        This simulates what happens when:\n        1. User starts gunicorn with gevent worker\n        2. gunicorn.arbiter is imported (does NOT import concurrent.futures)\n        3. User's config file runs with monkey.patch_all()\n        4. concurrent.futures is imported later (after patching)\n\n        The result: concurrent.futures locks ARE properly patched.\n        \"\"\"\n        code = textwrap.dedent(\"\"\"\n            import sys\n\n            # Step 1: User starts gunicorn - gunicorn.arbiter gets imported\n            # With the lazy import fix, this does NOT import concurrent.futures\n            import gunicorn.arbiter\n\n            # Step 2: Verify concurrent.futures was NOT imported yet\n            assert 'concurrent.futures' not in sys.modules, \\\n                \"concurrent.futures should NOT have been imported by arbiter\"\n\n            # Step 3: Now user's config file runs with monkey.patch_all()\n            # This happens BEFORE concurrent.futures is imported - correct order!\n            from gevent import monkey\n            monkey.patch_all()\n\n            # Step 4: Now import concurrent.futures (after patching)\n            from concurrent.futures import thread as futures_thread\n            lock = futures_thread._global_shutdown_lock\n\n            from gevent.thread import LockType as GeventLockType\n            is_gevent_lock = isinstance(lock, GeventLockType)\n\n            print(f\"RESULT:is_gevent_lock={is_gevent_lock}\")\n            print(f\"RESULT:lock_type={type(lock)}\")\n        \"\"\")\n\n        result = subprocess.run(\n            [sys.executable, \"-c\", code],\n            capture_output=True,\n            text=True\n        )\n\n        stdout = result.stdout.strip()\n        stderr = result.stderr.strip()\n\n        # Allow for the test to run even if gevent isn't available in subprocess\n        if \"ModuleNotFoundError\" in stderr or \"ImportError\" in stderr:\n            pytest.skip(\"gevent not available in subprocess\")\n\n        assert \"RESULT:is_gevent_lock=\" in stdout, \\\n            f\"Test script failed: stdout={stdout}, stderr={stderr}\"\n\n        is_gevent_lock = \"RESULT:is_gevent_lock=True\" in stdout\n\n        # The lock IS properly patched because:\n        # 1. gunicorn.arbiter no longer imports concurrent.futures at module load\n        # 2. monkey.patch_all() runs before concurrent.futures is imported\n        # 3. concurrent.futures gets the patched threading primitives\n        assert is_gevent_lock is True, (\n            \"Lock should be a gevent lock when gunicorn.arbiter is imported \"\n            \"before monkey.patch_all() - the dirty module should be lazily imported.\"\n        )\n\n    def test_gevent_config_file_patching_scenario(self):\n        \"\"\"Test the exact scenario from the bug report gist.\n\n        This reproduces the test case from:\n        https://gist.github.com/markjm/9f724364619c519892e8111fe6520ca6\n\n        The gist simulates a gunicorn config file that:\n        1. Calls monkey.patch_all()\n        2. Checks if locks in concurrent.futures are properly patched\n\n        With the fix, both locks (before and after importing concurrent.futures)\n        should be gevent locks because monkey.patch_all() runs before any\n        concurrent.futures import.\n        \"\"\"\n        code = textwrap.dedent(\"\"\"\n            import sys\n\n            # Simulate gunicorn startup - import arbiter first\n            # (this should NOT import concurrent.futures anymore)\n            import gunicorn.arbiter\n\n            # === This simulates a gunicorn config file (like echo.py from the gist) ===\n\n            # Config file starts by patching\n            from gevent import monkey\n            monkey.patch_all()\n            # print(\"[INFO] gevent.monkey.patch_all() called\")\n\n            # Now access concurrent.futures (after patching)\n            from concurrent.futures import thread as futures_thread\n            lock_after_patch = futures_thread._global_shutdown_lock\n\n            # Also create a new lock to compare\n            import threading\n            new_lock = threading.Lock()\n\n            from gevent.thread import LockType as GeventLockType\n            import _thread\n\n            # Check both locks\n            after_is_gevent = isinstance(lock_after_patch, GeventLockType)\n            after_module = type(lock_after_patch).__module__\n            new_is_gevent = isinstance(new_lock, GeventLockType)\n            new_module = type(new_lock).__module__\n\n            # Print comparison table like the gist\n            print(\"=== LOCK COMPARISON TABLE ===\")\n            print(f\"CF Lock Type: {type(lock_after_patch)}\")\n            print(f\"CF Lock Module: {after_module}\")\n            print(f\"CF Is GeventLockType: {after_is_gevent}\")\n            print(f\"New Lock Type: {type(new_lock)}\")\n            print(f\"New Lock Module: {new_module}\")\n            print(f\"New Is GeventLockType: {new_is_gevent}\")\n\n            # Results for parsing\n            print(f\"RESULT:cf_is_gevent={after_is_gevent}\")\n            print(f\"RESULT:cf_module={after_module}\")\n            print(f\"RESULT:new_is_gevent={new_is_gevent}\")\n        \"\"\")\n\n        result = subprocess.run(\n            [sys.executable, \"-c\", code],\n            capture_output=True,\n            text=True\n        )\n\n        stdout = result.stdout.strip()\n        stderr = result.stderr.strip()\n\n        if \"ModuleNotFoundError\" in stderr or \"ImportError\" in stderr:\n            pytest.skip(\"gevent not available in subprocess\")\n\n        assert \"RESULT:cf_is_gevent=\" in stdout, \\\n            f\"Test script failed: stdout={stdout}, stderr={stderr}\"\n\n        # Parse results\n        cf_is_gevent = \"RESULT:cf_is_gevent=True\" in stdout\n        new_is_gevent = \"RESULT:new_is_gevent=True\" in stdout\n\n        # With the fix, BOTH locks should be gevent locks\n        # This matches the expected v24 behavior from the gist\n        assert cf_is_gevent is True, (\n            \"concurrent.futures lock should be a gevent lock. \"\n            \"This indicates monkey.patch_all() ran before concurrent.futures was imported.\"\n        )\n        assert new_is_gevent is True, (\n            \"New threading.Lock should be a gevent lock after monkey.patch_all()\"\n        )\n"
  },
  {
    "path": "tests/workers/test_geventlet.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nimport pytest\nimport sys\nfrom unittest import mock\n\n\ndef test_import():\n    \"\"\"Test that the eventlet worker module can be imported.\"\"\"\n    try:\n        import eventlet\n    except AttributeError:\n        if (3, 13) > sys.version_info >= (3, 12):\n            pytest.skip(\"Ignoring eventlet failures on Python 3.12\")\n        raise\n    __import__('gunicorn.workers.geventlet')\n\n\nclass TestVersionRequirement:\n    \"\"\"Tests for eventlet version requirement checks.\"\"\"\n\n    def test_import_error_message(self):\n        \"\"\"Test that ImportError gives correct version message.\"\"\"\n        with mock.patch.dict('sys.modules', {'eventlet': None}):\n            # Clear cached module if present\n            sys.modules.pop('gunicorn.workers.geventlet', None)\n            with pytest.raises(RuntimeError, match=\"eventlet 0.40.3\"):\n                import importlib\n                import gunicorn.workers.geventlet\n                importlib.reload(gunicorn.workers.geventlet)\n\n    def test_version_check_requires_0_40_3(self):\n        \"\"\"Test that version check requires eventlet 0.40.3 or higher.\"\"\"\n        try:\n            import eventlet\n        except (ImportError, AttributeError):\n            pytest.skip(\"eventlet not available\")\n\n        from packaging.version import parse as parse_version\n        min_version = parse_version('0.40.3')\n        current_version = parse_version(eventlet.__version__)\n\n        # If we got this far, the import succeeded, meaning version is sufficient\n        assert current_version >= min_version\n\n\n@pytest.fixture\ndef eventlet_worker():\n    \"\"\"Fixture to create an EventletWorker instance for testing.\"\"\"\n    try:\n        import eventlet\n    except (ImportError, AttributeError):\n        pytest.skip(\"eventlet not available\")\n\n    from gunicorn.workers.geventlet import EventletWorker\n\n    # Create a minimal mock config\n    cfg = mock.MagicMock()\n    cfg.keepalive = 2\n    cfg.graceful_timeout = 30\n    cfg.is_ssl = False\n    cfg.worker_connections = 1000\n\n    # Create worker with mocked dependencies\n    worker = EventletWorker.__new__(EventletWorker)\n    worker.cfg = cfg\n    worker.alive = True\n    worker.sockets = []\n    worker.log = mock.MagicMock()\n\n    return worker\n\n\nclass TestEventletWorker:\n    \"\"\"Tests for EventletWorker class.\"\"\"\n\n    def test_worker_class_exists(self):\n        \"\"\"Test that EventletWorker class is properly defined.\"\"\"\n        try:\n            import eventlet\n        except (ImportError, AttributeError):\n            pytest.skip(\"eventlet not available\")\n\n        from gunicorn.workers.geventlet import EventletWorker\n        from gunicorn.workers.base_async import AsyncWorker\n\n        assert issubclass(EventletWorker, AsyncWorker)\n\n    def test_patch_method_calls_use_hub(self, eventlet_worker):\n        \"\"\"Test that patch() calls hubs.use_hub().\n\n        hubs.use_hub() must be called in patch() (after fork) because it creates\n        OS resources like kqueue that don't survive fork.\n        \"\"\"\n        from eventlet import hubs\n\n        with mock.patch.object(hubs, 'use_hub') as mock_use_hub:\n            with mock.patch('gunicorn.workers.geventlet.patch_sendfile'):\n                eventlet_worker.patch()\n\n        mock_use_hub.assert_called_once()\n\n    def test_patch_method_calls_patch_sendfile(self, eventlet_worker):\n        \"\"\"Test that patch() calls patch_sendfile().\"\"\"\n        from eventlet import hubs\n\n        with mock.patch.object(hubs, 'use_hub'):\n            with mock.patch('gunicorn.workers.geventlet.patch_sendfile') as mock_sf:\n                eventlet_worker.patch()\n\n        mock_sf.assert_called_once()\n\n    def test_monkey_patch_called_at_import_time(self):\n        \"\"\"Test that monkey_patch is called at module import time.\n\n        Note: hubs.use_hub() and eventlet.monkey_patch() are called at module\n        import time (not in patch()) to ensure all imports are properly patched.\n        This test verifies the module was patched by checking eventlet state.\n        \"\"\"\n        try:\n            import eventlet\n        except (ImportError, AttributeError):\n            pytest.skip(\"eventlet not available\")\n\n        # Verify eventlet has been patched by checking that socket is patched\n        import socket\n        from eventlet.greenio import GreenSocket\n\n        # After monkey patching, socket.socket should be GreenSocket\n        assert socket.socket is GreenSocket\n\n    def test_timeout_ctx_returns_eventlet_timeout(self, eventlet_worker):\n        \"\"\"Test that timeout_ctx() returns an eventlet.Timeout.\"\"\"\n        import eventlet\n\n        timeout = eventlet_worker.timeout_ctx()\n        assert isinstance(timeout, eventlet.Timeout)\n\n    def test_timeout_ctx_uses_keepalive_config(self, eventlet_worker):\n        \"\"\"Test that timeout_ctx() uses cfg.keepalive value.\"\"\"\n        import eventlet\n\n        eventlet_worker.cfg.keepalive = 5\n        with mock.patch.object(eventlet, 'Timeout') as mock_timeout:\n            eventlet_worker.timeout_ctx()\n\n        mock_timeout.assert_called_once_with(5, False)\n\n    def test_timeout_ctx_with_no_keepalive(self, eventlet_worker):\n        \"\"\"Test that timeout_ctx() handles no keepalive (None or 0).\"\"\"\n        import eventlet\n\n        eventlet_worker.cfg.keepalive = 0\n        with mock.patch.object(eventlet, 'Timeout') as mock_timeout:\n            eventlet_worker.timeout_ctx()\n\n        mock_timeout.assert_called_once_with(None, False)\n\n    def test_handle_quit_spawns_greenthread(self, eventlet_worker):\n        \"\"\"Test that handle_quit() spawns a greenthread.\"\"\"\n        import eventlet\n\n        with mock.patch.object(eventlet, 'spawn') as mock_spawn:\n            eventlet_worker.handle_quit(None, None)\n\n        mock_spawn.assert_called_once()\n\n    def test_handle_usr1_spawns_greenthread(self, eventlet_worker):\n        \"\"\"Test that handle_usr1() spawns a greenthread.\"\"\"\n        import eventlet\n\n        with mock.patch.object(eventlet, 'spawn') as mock_spawn:\n            eventlet_worker.handle_usr1(None, None)\n\n        mock_spawn.assert_called_once()\n\n    def test_handle_wraps_ssl_when_configured(self, eventlet_worker):\n        \"\"\"Test that handle() wraps socket with SSL when is_ssl is True.\"\"\"\n        from gunicorn.workers import geventlet\n\n        eventlet_worker.cfg.is_ssl = True\n        mock_client = mock.MagicMock()\n        mock_listener = mock.MagicMock()\n\n        with mock.patch.object(geventlet, 'ssl_wrap_socket') as mock_ssl:\n            mock_ssl.return_value = mock_client\n            with mock.patch('gunicorn.workers.base_async.AsyncWorker.handle'):\n                eventlet_worker.handle(mock_listener, mock_client, ('127.0.0.1', 8000))\n\n        mock_ssl.assert_called_once_with(mock_client, eventlet_worker.cfg)\n\n    def test_handle_no_ssl_when_not_configured(self, eventlet_worker):\n        \"\"\"Test that handle() does not wrap SSL when is_ssl is False.\"\"\"\n        from gunicorn.workers import geventlet\n\n        eventlet_worker.cfg.is_ssl = False\n        mock_client = mock.MagicMock()\n        mock_listener = mock.MagicMock()\n\n        with mock.patch.object(geventlet, 'ssl_wrap_socket') as mock_ssl:\n            with mock.patch('gunicorn.workers.base_async.AsyncWorker.handle'):\n                eventlet_worker.handle(mock_listener, mock_client, ('127.0.0.1', 8000))\n\n        mock_ssl.assert_not_called()\n\n\nclass TestAlreadyHandled:\n    \"\"\"Tests for is_already_handled() method.\"\"\"\n\n    def test_is_already_handled_new_style(self, eventlet_worker):\n        \"\"\"Test is_already_handled with eventlet >= 0.30.3 (WSGI_LOCAL).\"\"\"\n        from gunicorn.workers import geventlet\n\n        # Mock the new-style WSGI_LOCAL.already_handled\n        mock_wsgi_local = mock.MagicMock()\n        mock_wsgi_local.already_handled = True\n\n        with mock.patch.object(geventlet, 'EVENTLET_WSGI_LOCAL', mock_wsgi_local):\n            with pytest.raises(StopIteration):\n                eventlet_worker.is_already_handled(mock.MagicMock())\n\n    def test_is_already_handled_old_style(self, eventlet_worker):\n        \"\"\"Test is_already_handled with eventlet < 0.30.3 (ALREADY_HANDLED).\"\"\"\n        from gunicorn.workers import geventlet\n\n        sentinel = object()\n\n        with mock.patch.object(geventlet, 'EVENTLET_WSGI_LOCAL', None):\n            with mock.patch.object(geventlet, 'EVENTLET_ALREADY_HANDLED', sentinel):\n                with pytest.raises(StopIteration):\n                    eventlet_worker.is_already_handled(sentinel)\n\n    def test_is_already_handled_returns_parent_result(self, eventlet_worker):\n        \"\"\"Test is_already_handled falls through to parent when not handled.\"\"\"\n        from gunicorn.workers import geventlet\n\n        with mock.patch.object(geventlet, 'EVENTLET_WSGI_LOCAL', None):\n            with mock.patch.object(geventlet, 'EVENTLET_ALREADY_HANDLED', None):\n                with mock.patch('gunicorn.workers.base_async.AsyncWorker.is_already_handled') as mock_parent:\n                    mock_parent.return_value = False\n                    result = eventlet_worker.is_already_handled(mock.MagicMock())\n\n        assert result is False\n        mock_parent.assert_called_once()\n\n\nclass TestPatchSendfile:\n    \"\"\"Tests for patch_sendfile() function.\"\"\"\n\n    def test_patch_sendfile_adds_method_when_missing(self):\n        \"\"\"Test that patch_sendfile adds sendfile to GreenSocket if missing.\"\"\"\n        try:\n            import eventlet\n        except (ImportError, AttributeError):\n            pytest.skip(\"eventlet not available\")\n\n        from gunicorn.workers.geventlet import patch_sendfile, _eventlet_socket_sendfile\n        from eventlet.greenio import GreenSocket\n\n        # Remove sendfile if it exists\n        original = getattr(GreenSocket, 'sendfile', None)\n        if hasattr(GreenSocket, 'sendfile'):\n            delattr(GreenSocket, 'sendfile')\n\n        try:\n            patch_sendfile()\n            assert hasattr(GreenSocket, 'sendfile')\n            assert GreenSocket.sendfile == _eventlet_socket_sendfile\n        finally:\n            # Restore original state\n            if original is not None:\n                GreenSocket.sendfile = original\n            elif hasattr(GreenSocket, 'sendfile'):\n                delattr(GreenSocket, 'sendfile')\n\n    def test_patch_sendfile_preserves_existing_method(self):\n        \"\"\"Test that patch_sendfile does not override existing sendfile.\"\"\"\n        try:\n            import eventlet\n        except (ImportError, AttributeError):\n            pytest.skip(\"eventlet not available\")\n\n        from gunicorn.workers.geventlet import patch_sendfile\n        from eventlet.greenio import GreenSocket\n\n        # If sendfile exists, it should be preserved\n        if hasattr(GreenSocket, 'sendfile'):\n            original = GreenSocket.sendfile\n            patch_sendfile()\n            assert GreenSocket.sendfile == original\n\n\nclass TestEventletSocketSendfile:\n    \"\"\"Tests for _eventlet_socket_sendfile() function.\"\"\"\n\n    def test_sendfile_raises_on_non_blocking(self):\n        \"\"\"Test that sendfile raises ValueError for non-blocking sockets.\"\"\"\n        try:\n            import eventlet\n        except (ImportError, AttributeError):\n            pytest.skip(\"eventlet not available\")\n\n        from gunicorn.workers.geventlet import _eventlet_socket_sendfile\n\n        mock_socket = mock.MagicMock()\n        mock_socket.gettimeout.return_value = 0\n\n        with pytest.raises(ValueError, match=\"non-blocking\"):\n            _eventlet_socket_sendfile(mock_socket, mock.MagicMock())\n\n    def test_sendfile_seeks_to_offset(self):\n        \"\"\"Test that sendfile seeks to offset if provided.\"\"\"\n        try:\n            import eventlet\n        except (ImportError, AttributeError):\n            pytest.skip(\"eventlet not available\")\n\n        from gunicorn.workers.geventlet import _eventlet_socket_sendfile\n\n        mock_socket = mock.MagicMock()\n        mock_socket.gettimeout.return_value = 1\n        mock_file = mock.MagicMock()\n        mock_file.read.return_value = b''\n\n        _eventlet_socket_sendfile(mock_socket, mock_file, offset=100)\n\n        mock_file.seek.assert_any_call(100)\n\n    def test_sendfile_returns_total_sent(self):\n        \"\"\"Test that sendfile returns the total bytes sent.\"\"\"\n        try:\n            import eventlet\n        except (ImportError, AttributeError):\n            pytest.skip(\"eventlet not available\")\n\n        from gunicorn.workers.geventlet import _eventlet_socket_sendfile\n\n        mock_socket = mock.MagicMock()\n        mock_socket.gettimeout.return_value = 1\n        mock_socket.send.return_value = 10\n\n        mock_file = mock.MagicMock()\n        mock_file.read.side_effect = [b'x' * 10, b'']\n\n        result = _eventlet_socket_sendfile(mock_socket, mock_file)\n\n        assert result == 10\n\n\nclass TestEventletServe:\n    \"\"\"Tests for _eventlet_serve() function.\"\"\"\n\n    def test_serve_creates_green_pool(self):\n        \"\"\"Test that _eventlet_serve creates a GreenPool.\"\"\"\n        try:\n            import eventlet\n        except (ImportError, AttributeError):\n            pytest.skip(\"eventlet not available\")\n\n        from gunicorn.workers.geventlet import _eventlet_serve\n\n        mock_sock = mock.MagicMock()\n        mock_sock.accept.side_effect = eventlet.StopServe()\n\n        with mock.patch.object(eventlet.greenpool, 'GreenPool') as mock_pool:\n            mock_pool_instance = mock.MagicMock()\n            mock_pool.return_value = mock_pool_instance\n            mock_pool_instance.waitall.return_value = None\n\n            _eventlet_serve(mock_sock, mock.MagicMock(), 100)\n\n        mock_pool.assert_called_once_with(100)\n\n\nclass TestEventletStop:\n    \"\"\"Tests for _eventlet_stop() function.\"\"\"\n\n    def test_stop_waits_for_client(self):\n        \"\"\"Test that _eventlet_stop waits for the client greenlet.\"\"\"\n        try:\n            import eventlet\n        except (ImportError, AttributeError):\n            pytest.skip(\"eventlet not available\")\n\n        from gunicorn.workers.geventlet import _eventlet_stop\n\n        mock_client = mock.MagicMock()\n        mock_server = mock.MagicMock()\n        mock_conn = mock.MagicMock()\n\n        _eventlet_stop(mock_client, mock_server, mock_conn)\n\n        mock_client.wait.assert_called_once()\n        mock_conn.close.assert_called_once()\n\n    def test_stop_closes_connection_on_greenlet_exit(self):\n        \"\"\"Test that connection is closed even on GreenletExit.\"\"\"\n        try:\n            import eventlet\n            import greenlet\n        except (ImportError, AttributeError):\n            pytest.skip(\"eventlet not available\")\n\n        from gunicorn.workers.geventlet import _eventlet_stop\n\n        mock_client = mock.MagicMock()\n        mock_client.wait.side_effect = greenlet.GreenletExit()\n        mock_server = mock.MagicMock()\n        mock_conn = mock.MagicMock()\n\n        # Should not raise\n        _eventlet_stop(mock_client, mock_server, mock_conn)\n\n        mock_conn.close.assert_called_once()\n"
  },
  {
    "path": "tests/workers/test_ggevent.py",
    "content": "#\n# This file is part of gunicorn released under the MIT license.\n# See the NOTICE for more information.\n\nfrom unittest import mock\n\nimport pytest\n\ntry:\n    import gevent\n    HAS_GEVENT = True\nexcept ImportError:\n    HAS_GEVENT = False\n\npytestmark = pytest.mark.skipif(not HAS_GEVENT, reason=\"gevent not installed\")\n\n\ndef test_import():\n    __import__('gunicorn.workers.ggevent')\n\n\ndef test_version_requirement():\n    \"\"\"Test that gevent 24.10.1+ is required.\"\"\"\n    from gunicorn.workers import ggevent\n    from packaging.version import parse as parse_version\n    assert parse_version(gevent.__version__) >= parse_version('24.10.1')\n\n\nclass TestGeventWorkerInit:\n    \"\"\"Test GeventWorker initialization.\"\"\"\n\n    def test_worker_has_no_server_class(self):\n        \"\"\"Test that GeventWorker has no server_class by default.\"\"\"\n        from gunicorn.workers.ggevent import GeventWorker\n        assert GeventWorker.server_class is None\n\n    def test_worker_has_no_wsgi_handler(self):\n        \"\"\"Test that GeventWorker has no wsgi_handler by default.\"\"\"\n        from gunicorn.workers.ggevent import GeventWorker\n        assert GeventWorker.wsgi_handler is None\n\n    def test_init_process_patches_and_reinits(self):\n        \"\"\"Test that init_process calls patch and reinits the hub.\"\"\"\n        from gunicorn.workers.ggevent import GeventWorker\n\n        worker = mock.Mock(spec=GeventWorker)\n        worker.sockets = []\n\n        with mock.patch('gunicorn.workers.ggevent.hub') as mock_hub, \\\n             mock.patch.object(GeventWorker.__bases__[0], 'init_process'):\n            GeventWorker.init_process(worker)\n\n            # Verify patch was called\n            worker.patch.assert_called_once()\n            mock_hub.reinit.assert_called_once()\n\n\nclass TestGeventWorkerRun:\n    \"\"\"Test GeventWorker run method.\"\"\"\n\n    def test_run_creates_stream_servers(self):\n        \"\"\"Test that run creates StreamServer instances for each socket.\"\"\"\n        from gunicorn.workers.ggevent import GeventWorker\n\n        worker = mock.Mock(spec=GeventWorker)\n        worker.sockets = [mock.Mock()]\n        worker.cfg = mock.Mock(is_ssl=False, workers=1, graceful_timeout=30)\n        worker.server_class = None\n        worker.worker_connections = 1000\n\n        # Make alive return True once, then False to exit the loop\n        worker.alive = False\n\n        with mock.patch('gunicorn.workers.ggevent.Pool') as mock_pool, \\\n             mock.patch('gunicorn.workers.ggevent.StreamServer') as mock_server_cls, \\\n             mock.patch('gunicorn.workers.ggevent.gevent') as mock_gevent:\n\n            mock_server = mock.Mock()\n            mock_server.pool = mock.Mock()\n            mock_server.pool.free_count.return_value = mock_server.pool.size\n            mock_server_cls.return_value = mock_server\n\n            GeventWorker.run(worker)\n\n            mock_server_cls.assert_called_once()\n            mock_server.start.assert_called_once()\n            mock_server.close.assert_called_once()\n\n    def test_run_with_ssl(self):\n        \"\"\"Test that run configures SSL context when is_ssl is True.\"\"\"\n        from gunicorn.workers.ggevent import GeventWorker\n\n        worker = mock.Mock(spec=GeventWorker)\n        worker.sockets = [mock.Mock()]\n        worker.cfg = mock.Mock(is_ssl=True, workers=1, graceful_timeout=30)\n        worker.server_class = None\n        worker.worker_connections = 1000\n        worker.alive = False\n\n        with mock.patch('gunicorn.workers.ggevent.Pool'), \\\n             mock.patch('gunicorn.workers.ggevent.StreamServer') as mock_server_cls, \\\n             mock.patch('gunicorn.workers.ggevent.gevent'), \\\n             mock.patch('gunicorn.workers.ggevent.ssl_context') as mock_ssl_ctx:\n\n            mock_server = mock.Mock()\n            mock_server.pool = mock.Mock()\n            mock_server.pool.free_count.return_value = mock_server.pool.size\n            mock_server_cls.return_value = mock_server\n            mock_ssl_ctx.return_value = mock.Mock()\n\n            GeventWorker.run(worker)\n\n            mock_ssl_ctx.assert_called_once_with(worker.cfg)\n            # Verify ssl_context was passed to StreamServer\n            call_kwargs = mock_server_cls.call_args[1]\n            assert 'ssl_context' in call_kwargs\n\n\nclass TestSignalHandling:\n    \"\"\"Test signal handling in GeventWorker.\"\"\"\n\n    def test_handle_quit_spawns_greenlet(self):\n        \"\"\"Test that handle_quit spawns a greenlet instead of blocking.\"\"\"\n        from gunicorn.workers.ggevent import GeventWorker\n\n        worker = mock.Mock(spec=GeventWorker)\n\n        with mock.patch('gunicorn.workers.ggevent.gevent') as mock_gevent:\n            GeventWorker.handle_quit(worker, mock.Mock(), mock.Mock())\n            mock_gevent.spawn.assert_called_once()\n\n    def test_handle_usr1_spawns_greenlet(self):\n        \"\"\"Test that handle_usr1 spawns a greenlet instead of blocking.\"\"\"\n        from gunicorn.workers.ggevent import GeventWorker\n\n        worker = mock.Mock(spec=GeventWorker)\n\n        with mock.patch('gunicorn.workers.ggevent.gevent') as mock_gevent:\n            GeventWorker.handle_usr1(worker, mock.Mock(), mock.Mock())\n            mock_gevent.spawn.assert_called_once()\n\n    def test_notify_exits_on_parent_change(self):\n        \"\"\"Test that notify exits when parent PID changes.\"\"\"\n        from gunicorn.workers.ggevent import GeventWorker\n\n        worker = mock.Mock(spec=GeventWorker)\n        worker.ppid = 1234\n        worker.log = mock.Mock()\n\n        with mock.patch('gunicorn.workers.ggevent.os') as mock_os, \\\n             mock.patch.object(GeventWorker.__bases__[0], 'notify'):\n            mock_os.getppid.return_value = 5678  # Different PID\n\n            with pytest.raises(SystemExit):\n                GeventWorker.notify(worker)\n\n\nclass TestPyWSGIWorker:\n    \"\"\"Test PyWSGI-based worker classes.\"\"\"\n\n    def test_pywsgi_worker_has_server_class(self):\n        \"\"\"Test that GeventPyWSGIWorker has proper server_class.\"\"\"\n        from gunicorn.workers.ggevent import GeventPyWSGIWorker, PyWSGIServer\n        assert GeventPyWSGIWorker.server_class is PyWSGIServer\n\n    def test_pywsgi_worker_has_handler(self):\n        \"\"\"Test that GeventPyWSGIWorker has proper wsgi_handler.\"\"\"\n        from gunicorn.workers.ggevent import GeventPyWSGIWorker, PyWSGIHandler\n        assert GeventPyWSGIWorker.wsgi_handler is PyWSGIHandler\n\n    def test_pywsgi_handler_get_environ(self):\n        \"\"\"Test that PyWSGIHandler adds gunicorn-specific environ keys.\"\"\"\n        from gunicorn.workers.ggevent import PyWSGIHandler\n\n        handler = mock.Mock(spec=PyWSGIHandler)\n        handler.socket = mock.Mock()\n        handler.path = '/test/path'\n\n        # Mock the parent get_environ\n        with mock.patch.object(PyWSGIHandler.__bases__[0], 'get_environ', return_value={}):\n            env = PyWSGIHandler.get_environ(handler)\n            assert env['gunicorn.sock'] == handler.socket\n            assert env['RAW_URI'] == '/test/path'\n\n\nclass TestGeventResponse:\n    \"\"\"Test GeventResponse helper class.\"\"\"\n\n    def test_response_attributes(self):\n        \"\"\"Test GeventResponse stores status, headers, and sent.\"\"\"\n        from gunicorn.workers.ggevent import GeventResponse\n\n        resp = GeventResponse('200 OK', {'Content-Type': 'text/html'}, 1024)\n        assert resp.status == '200 OK'\n        assert resp.headers == {'Content-Type': 'text/html'}\n        assert resp.sent == 1024\n\n\nclass TestTimeoutContext:\n    \"\"\"Test timeout context manager.\"\"\"\n\n    def test_timeout_ctx_uses_keepalive(self):\n        \"\"\"Test that timeout_ctx uses cfg.keepalive.\"\"\"\n        from gunicorn.workers.ggevent import GeventWorker\n\n        worker = mock.Mock(spec=GeventWorker)\n        worker.cfg = mock.Mock(keepalive=30)\n\n        with mock.patch('gunicorn.workers.ggevent.gevent') as mock_gevent:\n            mock_timeout = mock.Mock()\n            mock_gevent.Timeout.return_value = mock_timeout\n\n            result = GeventWorker.timeout_ctx(worker)\n\n            mock_gevent.Timeout.assert_called_once_with(30, False)\n            assert result == mock_timeout\n"
  },
  {
    "path": "tox.ini",
    "content": "[tox]\nenvlist =\n  py{312,313},\n  lint,\n  pycodestyle,\n  run-entrypoint,\n  run-module,\n\n[testenv]\npackage = editable\ncommands = pytest --cov=gunicorn {posargs}\ndeps =\n  -rrequirements_test.txt\n\n[testenv:run-entrypoint]\npackage = wheel\ndeps =\n# entry point: console script (provided by setuptools from pyproject.toml)\ncommands = python -c 'import subprocess; cmd_out = subprocess.check_output([\"gunicorn\", \"--version\"])[:79].decode(\"utf-8\", errors=\"replace\"); print(cmd_out); assert cmd_out.startswith(\"gunicorn \")'\n\n[testenv:run-module]\npackage = wheel\ndeps =\n# runpy (provided by module.__main__)\ncommands = python -c 'import sys,subprocess; cmd_out = subprocess.check_output([sys.executable, \"-m\", \"gunicorn\", \"--version\"])[:79].decode(\"utf-8\", errors=\"replace\"); print(cmd_out); assert cmd_out.startswith(\"gunicorn \")'\n\n[testenv:lint]\nno_package = true\ncommands =\n  pylint -j0 \\\n    --max-line-length=120 \\\n    gunicorn \\\n    tests/test_arbiter.py \\\n    tests/test_config.py \\\n    tests/test_gthread.py \\\n    tests/test_http.py \\\n    tests/test_invalid_requests.py \\\n    tests/test_logger.py \\\n    tests/test_pidfile.py \\\n    tests/test_sock.py \\\n    tests/test_ssl.py \\\n    tests/test_statsd.py \\\n    tests/test_systemd.py \\\n    tests/test_util.py \\\n    tests/test_valid_requests.py\ndeps =\n  pylint==3.3.2\n\n[testenv:pycodestyle]\nno_package = true\ncommands =\n  pycodestyle gunicorn\ndeps =\n  pycodestyle\n\n[pycodestyle]\nmax-line-length = 120\nignore = E129,W503,W504,W606\n"
  }
]