Repository: sanic-org/sanic

Issue:
The Sanic built-in server blocks for exactly 90 seconds on status code 412
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Describe the bug

When responding to any request in the Sanic development server, it usually responds immediately. However, if the response has status code 412, it takes is very slow and takes exactly 90 seconds to respond, after the handler finishes.

This behavior does not happen when running Sanic with uvicorn. Only the official Sanic server.


### Code snippet

```python
import sanic

app = sanic.Sanic(__name__)

@app.get("/")
async def root(req: sanic.Request):
    status = int(req.args.get("status", "200"))
    return sanic.json({"message": "Hello World"}, status=status)
```

### Expected Behavior

```bash
sanic main:app --port 8051
```

Then:

```sh-session
$ curl http://localhost:8051
{"message":"Hello World"}
$ curl http://localhost:8051/?status=400  # fine
{"message":"Hello World"}
$ curl http://localhost:8051/?status=411  # fine
{"message":"Hello World"}
$ curl http://localhost:8051/?status=413  # fine
{"message":"Hello World"}
$ curl http://localhost:8051/?status=412
# stalls with no response for 90 seconds
```

### How do you run Sanic?

Sanic CLI

### Operating System

Linux

### Sanic Version

Sanic v23.6.0

### Additional context

I have reproduced this on both Linux and macOS. I have also reproduced this using both the Sanic CLI and the `Sanic.serve()` function programmatically.

Discussion:

Patch:
diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml
index f5e45e19..515de0fa 100644
--- a/.github/workflows/publish-release.yml
+++ b/.github/workflows/publish-release.yml
@@ -69,7 +69,7 @@ jobs:
           tags="${tag_year}.${tag_month}"
 
           if [[ "${tag_month}" == "12" ]]; then
-            tags+=",LTS"
+            tags+=",lts"
             echo "::notice::Tag ${tag} is LTS version"
           else
             echo "::notice::Tag ${tag} is not LTS version"
@@ -122,7 +122,7 @@ jobs:
     name: Publish Docker / Python ${{ matrix.python-version }}
     needs: [generate_info, publish_package]
     runs-on: ubuntu-latest
-    if: ${{ needs.generate_info.IS_TEST == 'false' }}
+    if: ${{ needs.generate_info.outputs.is-test == 'false' }}
     strategy:
       fail-fast: true
       matrix:
diff --git a/crowdin.yml b/crowdin.yml
new file mode 100644
index 00000000..e0c660db
--- /dev/null
+++ b/crowdin.yml
@@ -0,0 +1,3 @@
+files:
+  - source: /guide/content/en/**/*.md
+    translation: /guide/content/%two_letters_code%/**/%original_file_name%
diff --git a/readthedocs.yml b/readthedocs.yml
index 87320098..f6a89418 100644
--- a/readthedocs.yml
+++ b/readthedocs.yml
@@ -1,9 +1,12 @@
 version: 2
 python:
-   version: 3.8
    install:
       - method: pip
         path: .
         extra_requirements:
             - docs
-   system_packages: true
+
+build:
+  os: "ubuntu-22.04"
+  tools:
+    python: "3.8"
diff --git a/sanic/helpers.py b/sanic/helpers.py
index d7ac8968..fbdac97f 100644
--- a/sanic/helpers.py
+++ b/sanic/helpers.py
@@ -122,25 +122,6 @@ def is_hop_by_hop_header(header):
     return header.lower() in _HOP_BY_HOP_HEADERS
 
 
-def remove_entity_headers(headers, allowed=("content-location", "expires")):
-    """
-    Removes all the entity headers present in the headers given.
-    According to RFC 2616 Section 10.3.5,
-    Content-Location and Expires are allowed as for the
-    "strong cache validator".
-    https://tools.ietf.org/html/rfc2616#section-10.3.5
-
-    returns the headers without the entity headers
-    """
-    allowed = set([h.lower() for h in allowed])
-    headers = {
-        header: value
-        for header, value in headers.items()
-        if not is_entity_header(header) or header.lower() in allowed
-    }
-    return headers
-
-
 def import_string(module_name, package=None):
     """
     import a module or class by string path.
diff --git a/sanic/response/types.py b/sanic/response/types.py
index 9d8a8e73..8dbde38e 100644
--- a/sanic/response/types.py
+++ b/sanic/response/types.py
@@ -24,7 +24,6 @@ from sanic.helpers import (
     Default,
     _default,
     has_message_body,
-    remove_entity_headers,
 )
 from sanic.http import Http
 
@@ -104,9 +103,6 @@ class BaseHTTPResponse:
         Returns:
             Iterator[Tuple[bytes, bytes]]: A list of header tuples encoded in bytes for sending
         """  # noqa: E501
-        # TODO: Make a blacklist set of header names and then filter with that
-        if self.status in (304, 412):  # Not Modified, Precondition Failed
-            self.headers = remove_entity_headers(self.headers)
         if has_message_body(self.status):
             self.headers.setdefault("content-type", self.content_type)
         # Encode headers into bytes


Test patch:
diff --git a/tests/test_helpers.py b/tests/test_helpers.py
index cecdd7c6..8f62121b 100644
--- a/tests/test_helpers.py
+++ b/tests/test_helpers.py
@@ -41,28 +41,6 @@ def test_is_hop_by_hop_header():
         assert helpers.is_hop_by_hop_header(header) is expected
 
 
-def test_remove_entity_headers():
-    tests = (
-        ({}, {}),
-        ({"Allow": "GET, POST, HEAD"}, {}),
-        (
-            {
-                "Content-Type": "application/json",
-                "Expires": "Wed, 21 Oct 2015 07:28:00 GMT",
-                "Foo": "Bar",
-            },
-            {"Expires": "Wed, 21 Oct 2015 07:28:00 GMT", "Foo": "Bar"},
-        ),
-        (
-            {"Allow": "GET, POST, HEAD", "Content-Location": "/test"},
-            {"Content-Location": "/test"},
-        ),
-    )
-
-    for header, expected in tests:
-        assert helpers.remove_entity_headers(header) == expected
-
-
 def test_import_string_class():
     obj = helpers.import_string("sanic.config.Config")
     assert isinstance(obj, Config)
diff --git a/tests/test_response.py b/tests/test_response.py
index 42180b69..9f39b4e0 100644
--- a/tests/test_response.py
+++ b/tests/test_response.py
@@ -178,6 +178,10 @@ def json_app(app):
     async def unmodified_handler(request: Request):
         return json(JSON_DATA, status=304)
 
+    @app.get("/precondition")
+    async def precondition_handler(request: Request):
+        return json(JSON_DATA, status=412)
+
     @app.delete("/")
     async def delete_handler(request: Request):
         return json(None, status=204)
@@ -193,6 +197,10 @@ def test_json_response(json_app):
     assert response.text == json_dumps(JSON_DATA)
     assert response.json == JSON_DATA
 
+    request, response = json_app.test_client.get("/precondition")
+    assert response.status == 412
+    assert response.json == JSON_DATA
+
 
 def test_no_content(json_app):
     request, response = json_app.test_client.get("/no-content")