Repository: encode/uvicorn

Issue:
Interactions between --reload-dir, --reload-include, and --reload-exclude are not very intuitive or well explained (and may even be wrong)
### Checklist

- [X] The bug is reproducible against the latest release or `master`.
- [X] There are no similar issues or pull requests to fix it yet.

### Describe the bug

I am trying to get `uvicorn` to automatically reload in a dev environment based on source file changes and config file changes. I have two Python src directories (`/path/to/first_dir` and `/path/to/second_dir`) and a config directory (`/path/to/env_dir`) which contains `*.env` files that my application loads.

It is _**really**_ hard to understand what `uvicorn` intends to do, much less what it actually does with regard to tuned reload behavior. Consider:

``` sh
% uvicorn \
     --reload \
     --reload-exclude '*_flymake.py' \
     --reload-exclude 'flycheck_*.py' \
     --reload-dir /path/to/first_dir \
     --reload-dir /path/to/second_dir \
     mm.txns.main:app
```

~~The above will _**only**_ ignore files matching `*_flymake.py` and `flycheck_*.py` in `/path/to/first_dir`, _**not**_ `/path/to/second_dir`. So it looks like one needs to repeat the exclude patterns prior to each `--reload-dir`:~~ _UPDATE: This wasn't correct. It turns out_ nothing _was being excluded in this configuration because watchgod was not installed. This confusion is part of the bug._

``` sh
% uvicorn \
     --reload \
     --reload-exclude '*_flymake.py' \
     --reload-exclude 'flycheck_*.py' \
     --reload-dir /path/to/first_dir \
     --reload-exclude '*_flymake.py' \
     --reload-exclude 'flycheck_*.py' \
     --reload-dir /path/to/second_dir \
     mm.txns.main:app
```

~~There is no mention of the need for repeated uses of `--reload-exclude` _**in advance of**_ any `--reload-dir` options that I can find.~~

Further, (in what I am perceiving as a contradiction of the docs), this won't work:

``` sh
% uvicorn \
     --reload \
     --reload-exclude '*_flymake.py' \
     --reload-exclude 'flycheck_*.py' \
     --reload-dir /path/to/first_dir \
     --reload-exclude '*_flymake.py' \
     --reload-exclude 'flycheck_*.py' \
     --reload-dir /path/to/second_dir \
     --reload-include '*.env' \
     --reload-dir /path/to/env_dir \
     mm.txns.main:app
```

Adding, deleting, or modifying `/path/to/env_dir/foo.env` will _**not**_ trigger a reload.

The [documentation for ``--reload-exclude``](https://www.uvicorn.org/settings/#development) makes it clear (or at least strongly suggests) I should be able to expand the patterns matched for reload detection (**_emphasis_** mine):

> `--reload-exclude <glob-pattern>` - Specify a glob pattern to match files or directories which will excluded from watching. May be used multiple times. By default the following patterns are excluded: `.*, .py[cod], .sw.*, ~*`. **_These defaults can be overwritten by including them in `--reload-include`._**

That's not what I'm seeing by including `*.env` files. Then there's this, which seems to contradict the inclusion docs (**_emphasis_** mine):

> By default Uvicorn uses simple changes detection strategy that compares **_python files_** modification times few times a second. If … or you need watching of **_non python files_** you can install [watchgod](https://pypi.org/project/watchgod/) or install uvicorn with `uvicorn[standard]`, which will include watchgod.

Does that mean there's some "upper bound" of patterns uvicorn is capable of considering? Is it `*.py`? That can't be correct, because the counterexample explicitly states that `*.py[cod]` is excluded by default, but could be included via `--reload-include`. It's not clear where the limitations are, even after spending dozens of minutes reading, re-reading, re-re-reading, experimenting, etc.

If the answer is, "Use watchgod," I can't find any documentation of how to do that or what additional configuration steps I would need to meet my use case.

### Environment

[root@localhost vagrant]# uname -a
Linux localhost.localdomain 5.4.17-2136.300.7.el8uek.x86_64 #2 SMP Fri Oct 8 16:23:01 PDT 2021 x86_64 x86_64 x86_64 GNU/Linux
[root@localhost vagrant]# /opt/local/app/bin/uvicorn --version
Running uvicorn 0.16.0 with CPython 3.9.6 on Linux

### Additional context

I'm not the only person to be confused by this. [This StackOverflow question](https://stackoverflow.com/questions/64118680/reload-flag-with-uvicorn-can-we-exclude-certain-code#comment123408591_64118929) poses something very similar, but remains unanswered:

> Could you advise on how to exclude [\*.json] using this method while preserving the reload functionality? I tried: [uvicorn main:app --reload-exclude '\*.json'] however it generated this warning: WARNING: Current configuration will not reload as not all conditions are met, please refer to documentation.

When I [misfiled this against gunicorn](https://github.com/benoitc/gunicorn/issues/2722) in a fit of blindess, I noted that [this comment](https://github.com/benoitc/gunicorn/issues/2339#issuecomment-651270632) from @Andrew-Chen-Wang suggests there is some magic around merely making watchgod available to uvicorn, but fails to go into additional detail, so I have no way of verifying it (at least not without digging into a lot of code), much less figuring out how I can take advantage:

> … [I]f you have watchgod installed, uvicorn should automatically pick that up to make the reloading process better. …

He responded with:

> … In terms of resolving the issue, I believe you're not supposed to reuse reload-include several times. Instead, wrap all your values in double quotes and make them csv

If that's true, it's also not clear from the docs. It also doesn't help me, since I'm only using `--reload-include` once. (The above use of `--reload-exclude` multiple times does appear to get the behavior I want with respect to excluding various files/directories.)

_UPDATE: I'm pretty sure the single, comma-separated argument advice is wrong. [This strongly suggests](https://github.com/encode/uvicorn/blob/27f76476a14dac68a62dc2998717997607a36197/uvicorn/main.py#L85-L99) one should be able to provide `--reload-include` and `--reload-exclude` multiple times via the command line._

Discussion:
Andrew-Chen-Wang: LGTM. I'm sure Kludex will follow up since I'm not a maintainer.

Patch:
diff --git a/.github/ISSUE_TEMPLATE/1-issue.md b/.github/ISSUE_TEMPLATE/1-issue.md
new file mode 100644
index 0000000..0402b0b
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/1-issue.md
@@ -0,0 +1,16 @@
+---
+name: Issue
+about: Please only raise an issue if you've been advised to do so after discussion. Thanks! 🙏
+---
+
+The starting point for issues should usually be a discussion...
+
+https://github.com/encode/uvicorn/discussions
+
+Possible bugs may be raised as a "Potential Issue" discussion, feature requests may be raised as an "Ideas" discussion. We can then determine if the discussion needs to be escalated into an "Issue" or not.
+
+This will help us ensure that the "Issues" list properly reflects ongoing or needed work on the project.
+
+---
+
+- [ ] Initially raised as discussion #...
diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml
deleted file mode 100644
index 4f3f2df..0000000
--- a/.github/ISSUE_TEMPLATE/bug-report.yml
+++ /dev/null
@@ -1,75 +0,0 @@
-name: Bug report
-description: Report a bug to help improve this project
-body:
-  - type: checkboxes
-    id: checks
-    attributes:
-      label: Checklist
-      description: Please make sure you check all these items before submitting your bug report.
-      options:
-        - label: The bug is reproducible against the latest release or `master`.
-          required: true
-        - label: There are no similar issues or pull requests to fix it yet.
-          required: true
-  - type: textarea
-    id: describe
-    attributes:
-      label: Describe the bug
-      description: A clear and concise description of what the bug is.
-    validations:
-      required: true
-  - type: textarea
-    id: steps
-    attributes:
-      label: Steps to reproduce the bug
-      description: |
-        Provide a *minimal* example with steps to reproduce the bug locally.
-        NOTE: try to keep any external dependencies *at an absolute minimum*.
-        In other words, remove anything that doesn't make the bug go away.
-    validations:
-      required: false
-  - type: textarea
-    id: expected
-    attributes:
-      label: Expected behavior
-      description: A clear and concise description of what you expected to happen.
-    validations:
-      required: false
-  - type: textarea
-    id: actual
-    attributes:
-      label: Actual behavior
-      description: A clear and concise description of what actually happened.
-    validations:
-      required: false
-  - type: textarea
-    id: notes
-    attributes:
-      label: Debugging material
-      description: |
-        Any tracebacks, screenshots, etc. that can help understanding the problem.
-        NOTE:
-        - Please list tracebacks in full (don't truncate them).
-        - If relevant, consider turning on DEBUG or TRACE logs for additional details (see the Logging section on https://www.uvicorn.org/settings/ specifically the `log-level` flag).
-        - Consider using `<details>` to make tracebacks/logs collapsible if they're very large (see https://gist.github.com/ericclemmons/b146fe5da72ca1f706b2ef72a20ac39d).
-    validations:
-      required: false
-  - type: textarea
-    id: environment
-    attributes:
-      label: Environment
-      description: Describe your environment.
-      placeholder: |
-        - OS / Python / Uvicorn version: just run `uvicorn --version`
-        - The exact command you're running uvicorn with, all flags you passed included. If you run it with gunicorn please do the same. If there is a reverse-proxy involved and you cannot reproduce without it please give the minimal config of it to reproduce.
-    validations:
-      required: true
-  - type: textarea
-    id: additional
-    attributes:
-      label: Additional context
-      description: |
-        Any additional information that can help understanding the problem.
-        Eg. linked issues, or a description of what you were trying to achieve.
-    validations:
-      required: false
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 2ad6e8e..9c638cc 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,7 +1,11 @@
 # Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser
-blank_issues_enabled: true
+blank_issues_enabled: false
 contact_links:
-- name: Question
+- name: Discussions
+  url: https://github.com/encode/uvicorn/discussions
+  about: >
+    The "Discussions" forum is where you want to start. 💖
+- name: Chat
   url: https://gitter.im/encode/community
   about: >
-    Ask a question
+    Our community chat forum.
diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml
deleted file mode 100644
index 7ee6c77..0000000
--- a/.github/ISSUE_TEMPLATE/feature-request.yml
+++ /dev/null
@@ -1,36 +0,0 @@
-name: Feature Request
-description: Suggest an idea for a feature that you would like to see in uvicorn
-body:
-  - type: checkboxes
-    id: checks
-    attributes:
-      label: Checklist
-      description: Please make sure you check all these items before submitting your feature request.
-      options:
-        - label: There are no similar issues or pull requests for this yet.
-          required: true
-        - label: I discussed this idea on the [community chat](https://gitter.im/encode/community) and feedback is positive.
-          required: true
-  - type: textarea
-    id: problem
-    attributes:
-      label: Is your feature related to a problem? Please describe.
-      description: A clear and concise description of what you are trying to achieve.
-      placeholder: I want to be able to [...] but I can't because [...]
-  - type: textarea
-    id: solution
-    attributes:
-      label: Describe the solution you would like.
-      description: |
-        A clear and concise description of what you would want to happen.
-        For API changes, try to provide a code snippet of what you would like the API to look like.
-  - type: textarea
-    id: alternatives
-    attributes:
-      label: Describe alternatives you considered
-      description: Please describe any alternative solutions or features you've considered to solve your problem and why they wouldn't solve it.
-  - type: textarea
-    id: additional
-    attributes:
-      label: Additional context
-      description: Provide any additional context, screenshots, trace backs, etc. about the feature here.
diff --git a/docs/deployment.md b/docs/deployment.md
index 60d1af7..2224f98 100644
--- a/docs/deployment.md
+++ b/docs/deployment.md
@@ -39,12 +39,15 @@ Options:
                                   of using the current working directory.
   --reload-include TEXT           Set glob patterns to include while watching
                                   for files. Includes '*.py' by default; these
-                                  defaults can be overridden in `--reload-
-                                  exclude`.
+                                  defaults can be overridden with `--reload-
+                                  exclude`. This option has no effect unless
+                                  watchgod is installed.
   --reload-exclude TEXT           Set glob patterns to exclude while watching
                                   for files. Includes '.*, .py[cod], .sw.*,
                                   ~*' by default; these defaults can be
-                                  overridden in `--reload-include`.
+                                  overridden with `--reload-include`. This
+                                  option has no effect unless watchgod is
+                                  installed.
   --reload-delay FLOAT            Delay between previous and next check if
                                   application needs to be. Defaults to 0.25s.
                                   [default: 0.25]
diff --git a/docs/index.md b/docs/index.md
index 6f57b09..f9b79d5 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -108,12 +108,15 @@ Options:
                                   of using the current working directory.
   --reload-include TEXT           Set glob patterns to include while watching
                                   for files. Includes '*.py' by default; these
-                                  defaults can be overridden in `--reload-
-                                  exclude`.
+                                  defaults can be overridden with `--reload-
+                                  exclude`. This option has no effect unless
+                                  watchgod is installed.
   --reload-exclude TEXT           Set glob patterns to exclude while watching
                                   for files. Includes '.*, .py[cod], .sw.*,
                                   ~*' by default; these defaults can be
-                                  overridden in `--reload-include`.
+                                  overridden with `--reload-include`. This
+                                  option has no effect unless watchgod is
+                                  installed.
   --reload-delay FLOAT            Delay between previous and next check if
                                   application needs to be. Defaults to 0.25s.
                                   [default: 0.25]
diff --git a/docs/settings.md b/docs/settings.md
index 3edf4b3..25be523 100644
--- a/docs/settings.md
+++ b/docs/settings.md
@@ -13,6 +13,7 @@ For example, in case you want to run the app on port `5000`, just set the enviro
 !!! note
     CLI options and the arguments for `uvicorn.run()` take precedence over environment variables.
 
+    Also note that `UVICORN_*` prefixed settings cannot be used from within an environment configuration file. Using an environment configuration file with the `--env-file` flag is intended for configuring the ASGI application that uvicorn runs, rather than configuring uvicorn itself.
 
 ## Application
 
@@ -28,13 +29,20 @@ For example, in case you want to run the app on port `5000`, just set the enviro
 
 ## Development
 
-* `--reload` - Enable auto-reload.
+* `--reload` - Enable auto-reload. Uvicorn supports two versions of auto-reloading behavior enabled by this option. There are important differences between them.
 * `--reload-dir <path>` - Specify which directories to watch for python file changes. May be used multiple times. If unused, then by default the whole current directory will be watched. If you are running programmatically use `reload_dirs=[]` and pass a list of strings.
+
+### Reloading without watchgod
+
+If Uvicorn _cannot_ load [watchgod](https://pypi.org/project/watchgod/) at runtime, it will periodically look for changes in modification times to all `*.py` files (and only `*.py` files) inside of its monitored directories. See the `--reload-dir` option. Specifying other file extensions is not supported unless watchgod is installed. See the `--reload-include` and `--reload-exclude` options for details.
+
+### Reloading with watchgod
+
+For more nuanced control over which file modifications trigger reloads, install `uvicorn[standard]`, which includes watchgod as a dependency. Alternatively, install [watchgod](https://pypi.org/project/watchgod/) where Unvicorn can see it. This will enable the following options (which are otherwise ignored).
+
 * `--reload-include <glob-pattern>` - Specify a glob pattern to match files or directories which will be watched. May be used multiple times. By default the following patterns are included: `*.py`. These defaults can be overwritten by including them in `--reload-exclude`.
 * `--reload-exclude <glob-pattern>` - Specify a glob pattern to match files or directories which will excluded from watching. May be used multiple times. By default the following patterns are excluded: `.*, .py[cod], .sw.*, ~*`. These defaults can be overwritten by including them in `--reload-include`.
 
-By default Uvicorn uses simple changes detection strategy that compares python files modification times few times a second. If this approach doesn't work for your project (eg. because of its complexity), or you need watching of non python files you can install [watchgod](https://pypi.org/project/watchgod/) or install uvicorn with `uvicorn[standard]`, which will include watchgod.
-
 ## Production
 
 * `--workers <int>` - Use multiple worker processes. Defaults to the `$WEB_CONCURRENCY` environment variable if available, or 1.
diff --git a/scripts/coverage b/scripts/coverage
index ec9f1b8..0ff7aae 100755
--- a/scripts/coverage
+++ b/scripts/coverage
@@ -8,4 +8,4 @@ export SOURCE_FILES="uvicorn tests"
 
 set -x
 
-${PREFIX}coverage report --show-missing --skip-covered --fail-under=96.93
+${PREFIX}coverage report --show-missing --skip-covered --fail-under=96.87
diff --git a/uvicorn/main.py b/uvicorn/main.py
index cd853b6..0db1216 100644
--- a/uvicorn/main.py
+++ b/uvicorn/main.py
@@ -87,7 +87,8 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
     "reload_includes",
     multiple=True,
     help="Set glob patterns to include while watching for files. Includes '*.py' "
-    "by default; these defaults can be overridden in `--reload-exclude`.",
+    "by default; these defaults can be overridden with `--reload-exclude`. "
+    "This option has no effect unless watchgod is installed.",
 )
 @click.option(
     "--reload-exclude",
@@ -95,7 +96,8 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
     multiple=True,
     help="Set glob patterns to exclude while watching for files. Includes "
     "'.*, .py[cod], .sw.*, ~*' by default; these defaults can be overridden "
-    "in `--reload-include`.",
+    "with `--reload-include`. This option has no effect unless watchgod is "
+    "installed.",
 )
 @click.option(
     "--reload-delay",
diff --git a/uvicorn/protocols/http/h11_impl.py b/uvicorn/protocols/http/h11_impl.py
index 78e5ffd..a81aaea 100644
--- a/uvicorn/protocols/http/h11_impl.py
+++ b/uvicorn/protocols/http/h11_impl.py
@@ -130,7 +130,7 @@ class H11Protocol(asyncio.Protocol):
             except h11.RemoteProtocolError as exc:
                 msg = "Invalid HTTP request received."
                 self.logger.warning(msg, exc_info=exc)
-                self.transport.close()
+                self.send_400_response(msg)
                 return
             event_type = type(event)
 
@@ -225,28 +225,12 @@ class H11Protocol(asyncio.Protocol):
         if upgrade_value != b"websocket" or self.ws_protocol_class is None:
             msg = "Unsupported upgrade request."
             self.logger.warning(msg)
-
             from uvicorn.protocols.websockets.auto import AutoWebSocketsProtocol
 
-            if AutoWebSocketsProtocol is None:
+            if AutoWebSocketsProtocol is None:  # pragma: no cover
                 msg = "No supported WebSocket library detected. Please use 'pip install uvicorn[standard]', or install 'websockets' or 'wsproto' manually."  # noqa: E501
                 self.logger.warning(msg)
-
-            reason = STATUS_PHRASES[400]
-            headers = [
-                (b"content-type", b"text/plain; charset=utf-8"),
-                (b"connection", b"close"),
-            ]
-            event = h11.Response(status_code=400, headers=headers, reason=reason)
-            output = self.conn.send(event)
-            self.transport.write(output)
-            event = h11.Data(data=b"Unsupported upgrade request.")
-            output = self.conn.send(event)
-            self.transport.write(output)
-            event = h11.EndOfMessage()
-            output = self.conn.send(event)
-            self.transport.write(output)
-            self.transport.close()
+            self.send_400_response(msg)
             return
 
         if self.logger.level <= TRACE_LOG_LEVEL:
@@ -265,6 +249,24 @@ class H11Protocol(asyncio.Protocol):
         protocol.data_received(b"".join(output))
         self.transport.set_protocol(protocol)
 
+    def send_400_response(self, msg: str):
+
+        reason = STATUS_PHRASES[400]
+        headers = [
+            (b"content-type", b"text/plain; charset=utf-8"),
+            (b"connection", b"close"),
+        ]
+        event = h11.Response(status_code=400, headers=headers, reason=reason)
+        output = self.conn.send(event)
+        self.transport.write(output)
+        event = h11.Data(data=msg.encode("ascii"))
+        output = self.conn.send(event)
+        self.transport.write(output)
+        event = h11.EndOfMessage()
+        output = self.conn.send(event)
+        self.transport.write(output)
+        self.transport.close()
+
     def on_response_complete(self):
         self.server_state.total_requests += 1
 
diff --git a/uvicorn/protocols/http/httptools_impl.py b/uvicorn/protocols/http/httptools_impl.py
index e6183bb..838c177 100644
--- a/uvicorn/protocols/http/httptools_impl.py
+++ b/uvicorn/protocols/http/httptools_impl.py
@@ -126,7 +126,8 @@ class HttpToolsProtocol(asyncio.Protocol):
         except httptools.HttpParserError as exc:
             msg = "Invalid HTTP request received."
             self.logger.warning(msg, exc_info=exc)
-            self.transport.close()
+            self.send_400_response(msg)
+            return
         except httptools.HttpParserUpgrade:
             self.handle_upgrade()
 
@@ -139,27 +140,12 @@ class HttpToolsProtocol(asyncio.Protocol):
         if upgrade_value != b"websocket" or self.ws_protocol_class is None:
             msg = "Unsupported upgrade request."
             self.logger.warning(msg)
-
             from uvicorn.protocols.websockets.auto import AutoWebSocketsProtocol
 
-            if AutoWebSocketsProtocol is None:
+            if AutoWebSocketsProtocol is None:  # pragma: no cover
                 msg = "No supported WebSocket library detected. Please use 'pip install uvicorn[standard]', or install 'websockets' or 'wsproto' manually."  # noqa: E501
                 self.logger.warning(msg)
-
-            content = [STATUS_LINE[400]]
-            for name, value in self.default_headers:
-                content.extend([name, b": ", value, b"\r\n"])
-            content.extend(
-                [
-                    b"content-type: text/plain; charset=utf-8\r\n",
-                    b"content-length: " + str(len(msg)).encode("ascii") + b"\r\n",
-                    b"connection: close\r\n",
-                    b"\r\n",
-                    msg.encode("ascii"),
-                ]
-            )
-            self.transport.write(b"".join(content))
-            self.transport.close()
+            self.send_400_response(msg)
             return
 
         if self.logger.level <= TRACE_LOG_LEVEL:
@@ -179,6 +165,23 @@ class HttpToolsProtocol(asyncio.Protocol):
         protocol.data_received(b"".join(output))
         self.transport.set_protocol(protocol)
 
+    def send_400_response(self, msg: str):
+
+        content = [STATUS_LINE[400]]
+        for name, value in self.default_headers:
+            content.extend([name, b": ", value, b"\r\n"])
+        content.extend(
+            [
+                b"content-type: text/plain; charset=utf-8\r\n",
+                b"content-length: " + str(len(msg)).encode("ascii") + b"\r\n",
+                b"connection: close\r\n",
+                b"\r\n",
+                msg.encode("ascii"),
+            ]
+        )
+        self.transport.write(b"".join(content))
+        self.transport.close()
+
     # Parser callbacks
     def on_url(self, url):
         method = self.parser.get_method()
diff --git a/uvicorn/supervisors/statreload.py b/uvicorn/supervisors/statreload.py
index 2c9857e..8392e9f 100644
--- a/uvicorn/supervisors/statreload.py
+++ b/uvicorn/supervisors/statreload.py
@@ -20,6 +20,12 @@ class StatReload(BaseReload):
         self.reloader_name = "statreload"
         self.mtimes: Dict[Path, float] = {}
 
+        if config.reload_excludes or config.reload_includes:
+            logger.warning(
+                "--reload-include and --reload-exclude have no effect unless watchgod "
+                "is installed."
+            )
+
     def should_restart(self) -> bool:
         for file in self.iter_py_files():
             try:


Test patch:
diff --git a/tests/protocols/test_http.py b/tests/protocols/test_http.py
index 533b828..d5f3432 100644
--- a/tests/protocols/test_http.py
+++ b/tests/protocols/test_http.py
@@ -742,5 +742,5 @@ def test_invalid_http_request(request_line, protocol_cls, caplog, event_loop):
 
     with get_connected_protocol(app, protocol_cls, event_loop) as protocol:
         protocol.data_received(request)
-        assert not protocol.transport.buffer
-        assert "Invalid HTTP request received." in caplog.messages
+        assert b"HTTP/1.1 400 Bad Request" in protocol.transport.buffer
+        assert b"Invalid HTTP request received." in protocol.transport.buffer