{"html_url": "https://github.com/simonw/datasette/issues/1955#issuecomment-1356595665", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1955", "id": 1356595665, "node_id": "IC_kwDOBm6k_c5Q3AHR", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-12-18T00:58:16Z", "updated_at": "2022-12-18T00:58:16Z", "author_association": "OWNER", "body": "`pytest -m serial` on my Mac laptop also freezes:\r\n\r\n```\r\n(datasette) datasette % pytest -m serial\r\n======================================================= test session starts ========================================================\r\nplatform darwin -- Python 3.10.3, pytest-7.1.3, pluggy-1.0.0\r\nSQLite: 3.39.4\r\nrootdir: /Users/simon/Dropbox/Development/datasette, configfile: pytest.ini\r\nplugins: anyio-3.6.1, xdist-2.5.0, forked-1.4.0, asyncio-0.19.0, timeout-2.1.0, profiling-1.7.0\r\nasyncio: mode=strict\r\ncollected 1295 items / 1264 deselected / 31 selected \r\n\r\ntests/test_package.py . [ 3%]\r\ntests/test_cli_serve_server.py \r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1496652622, "label": "invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1955#issuecomment-1356596740", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1955", "id": 1356596740, "node_id": "IC_kwDOBm6k_c5Q3AYE", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-12-18T00:59:47Z", "updated_at": "2022-12-18T00:59:47Z", "author_association": "OWNER", "body": "Hitting `Ctrl+C` while using `--full-trace` gave me more clues:\r\n\r\n```\r\n% pytest -m serial tests/test_cli_serve_server.py --full-trace\r\n======================================================= test session starts ========================================================\r\nplatform darwin -- Python 3.10.3, pytest-7.1.3, pluggy-1.0.0\r\nSQLite: 3.39.4\r\nrootdir: /Users/simon/Dropbox/Development/datasette, configfile: pytest.ini\r\nplugins: anyio-3.6.1, xdist-2.5.0, forked-1.4.0, asyncio-0.19.0, timeout-2.1.0, profiling-1.7.0\r\nasyncio: mode=strict\r\ncollected 3 items \r\n\r\ntests/test_cli_serve_server.py ^C^C\r\n\r\n====================================================== no tests ran in 3.49s =======================================================\r\nTraceback (most recent call last):\r\n File \"/Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages/httpcore/_exceptions.py\", line 8, in map_exceptions\r\n yield\r\n File \"/Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages/httpcore/backends/sync.py\", line 86, in connect_tcp\r\n sock = socket.create_connection(\r\n File \"/Users/simon/.pyenv/versions/3.10.3/lib/python3.10/socket.py\", line 845, in create_connection\r\n raise err\r\n File \"/Users/simon/.pyenv/versions/3.10.3/lib/python3.10/socket.py\", line 833, in create_connection\r\n sock.connect(sa)\r\nConnectionRefusedError: [Errno 61] Connection refused\r\n[...]\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1496652622, "label": "invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1955#issuecomment-1356599930", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1955", "id": 1356599930, "node_id": "IC_kwDOBm6k_c5Q3BJ6", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-12-18T01:01:47Z", "updated_at": "2022-12-18T01:01:47Z", "author_association": "OWNER", "body": "I think that's this test: https://github.com/simonw/datasette/blob/63fb750f39cac6f49b451387fdff659ecd9edc5c/tests/test_cli_serve_server.py#L6-L13\r\n\r\nUsing this fixture: https://github.com/simonw/datasette/blob/63fb750f39cac6f49b451387fdff659ecd9edc5c/tests/conftest.py#L155-L175", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1496652622, "label": "invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1955#issuecomment-1356600917", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1955", "id": 1356600917, "node_id": "IC_kwDOBm6k_c5Q3BZV", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-12-18T01:02:26Z", "updated_at": "2022-12-18T01:02:26Z", "author_association": "OWNER", "body": "This bit here looks like it could hang!\r\n```python\r\n # Loop until port 8041 serves traffic \r\n while True: \r\n try: \r\n httpx.get(\"http://localhost:8041/\") \r\n break \r\n except httpx.ConnectError: \r\n time.sleep(0.1) \r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1496652622, "label": "invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1955#issuecomment-1356609095", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1955", "id": 1356609095, "node_id": "IC_kwDOBm6k_c5Q3DZH", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-12-18T01:10:43Z", "updated_at": "2022-12-18T01:10:43Z", "author_association": "OWNER", "body": "Improved version of that fixture:\r\n```diff\r\ndiff --git a/tests/conftest.py b/tests/conftest.py\r\nindex 44c44f87..69dee68b 100644\r\n--- a/tests/conftest.py\r\n+++ b/tests/conftest.py\r\n@@ -27,6 +27,17 @@ UNDOCUMENTED_PERMISSIONS = {\r\n _ds_client = None\r\n \r\n \r\n+def wait_until_responds(url, timeout=5.0, client=httpx, **kwargs):\r\n+ start = time.time()\r\n+ while time.time() - start < timeout:\r\n+ try:\r\n+ client.get(url, **kwargs)\r\n+ return\r\n+ except httpx.ConnectError:\r\n+ time.sleep(0.1)\r\n+ raise AssertionError(\"Timed out waiting for {} to respond\".format(url))\r\n+\r\n+\r\n @pytest_asyncio.fixture\r\n async def ds_client():\r\n from datasette.app import Datasette\r\n@@ -161,13 +172,7 @@ def ds_localhost_http_server():\r\n # Avoid FileNotFoundError: [Errno 2] No such file or directory:\r\n cwd=tempfile.gettempdir(),\r\n )\r\n- # Loop until port 8041 serves traffic\r\n- while True:\r\n- try:\r\n- httpx.get(\"http://localhost:8041/\")\r\n- break\r\n- except httpx.ConnectError:\r\n- time.sleep(0.1)\r\n+ wait_until_responds(\"http://localhost:8041/\")\r\n # Check it started successfully\r\n assert not ds_proc.poll(), ds_proc.stdout.read().decode(\"utf-8\")\r\n yield ds_proc\r\n@@ -202,12 +207,7 @@ def ds_localhost_https_server(tmp_path_factory):\r\n stderr=subprocess.STDOUT,\r\n cwd=tempfile.gettempdir(),\r\n )\r\n- while True:\r\n- try:\r\n- httpx.get(\"https://localhost:8042/\", verify=client_cert)\r\n- break\r\n- except httpx.ConnectError:\r\n- time.sleep(0.1)\r\n+ wait_until_responds(\"http://localhost:8042/\", verify=client_cert)\r\n # Check it started successfully\r\n assert not ds_proc.poll(), ds_proc.stdout.read().decode(\"utf-8\")\r\n yield ds_proc, client_cert\r\n@@ -231,12 +231,7 @@ def ds_unix_domain_socket_server(tmp_path_factory):\r\n # Poll until available\r\n transport = httpx.HTTPTransport(uds=uds)\r\n client = httpx.Client(transport=transport)\r\n- while True:\r\n- try:\r\n- client.get(\"http://localhost/_memory.json\")\r\n- break\r\n- except httpx.ConnectError:\r\n- time.sleep(0.1)\r\n+ wait_until_responds(\"http://localhost/_memory.json\", client=client)\r\n # Check it started successfully\r\n assert not ds_proc.poll(), ds_proc.stdout.read().decode(\"utf-8\")\r\n yield ds_proc, uds\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1496652622, "label": "invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1955#issuecomment-1356610089", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1955", "id": 1356610089, "node_id": "IC_kwDOBm6k_c5Q3Dop", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-12-18T01:12:39Z", "updated_at": "2022-12-18T01:12:39Z", "author_association": "OWNER", "body": "... and it turns out those tests saved me. Because I forgot to check if `datasette` would actually start a server correctly!\r\n\r\n```\r\n % datasette fixtures.db -p 8852\r\nINFO: Started server process [3538]\r\nINFO: Waiting for application startup.\r\nERROR: Exception in 'lifespan' protocol\r\nTraceback (most recent call last):\r\n File \"/Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages/uvicorn/lifespan/on.py\", line 86, in main\r\n await app(scope, self.receive, self.send)\r\n File \"/Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages/uvicorn/middleware/proxy_headers.py\", line 78, in __call__\r\n return await self.app(scope, receive, send)\r\n File \"/Users/simon/Dropbox/Development/datasette/datasette/utils/asgi.py\", line 437, in __call__\r\n return await self.asgi(scope, receive, send)\r\n File \"/Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages/asgi_csrf.py\", line 39, in app_wrapped_with_csrf\r\n await app(scope, receive, send)\r\n File \"/Users/simon/Dropbox/Development/datasette/datasette/app.py\", line 1457, in __call__\r\n path = scope[\"path\"]\r\nKeyError: 'path'\r\nERROR: Application startup failed. Exiting.\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1496652622, "label": "invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1955#issuecomment-1356618913", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1955", "id": 1356618913, "node_id": "IC_kwDOBm6k_c5Q3Fyh", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-12-18T01:29:05Z", "updated_at": "2022-12-18T01:29:05Z", "author_association": "OWNER", "body": "Now the only failure is in the `https` test - which fails like this (in CI and on my laptop):\r\n\r\n```\r\n message = str(exc)\r\n> raise mapped_exc(message) from exc\r\nE httpx.RemoteProtocolError: Server disconnected without sending a response.\r\n\r\n/opt/hostedtoolcache/Python/3.11.1/x64/lib/python3.11/site-packages/httpx/_transports/default.py:77: RemoteProtocolError\r\n=========================== short test summary info ============================\r\nERROR tests/test_cli_serve_server.py::test_serve_localhost_https - httpx.RemoteProtocolError: Server disconnected without sending a response.\r\n================= 30 passed, 1264 deselected, 1 error in 6.15s =================\r\n```\r\nThat's this test: https://github.com/simonw/datasette/blob/63fb750f39cac6f49b451387fdff659ecd9edc5c/tests/test_cli_serve_server.py#L16-L24\r\n\r\nAnd this fixture: https://github.com/simonw/datasette/blob/63fb750f39cac6f49b451387fdff659ecd9edc5c/tests/conftest.py#L178-L215\r\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1496652622, "label": "invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1955#issuecomment-1356620233", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1955", "id": 1356620233, "node_id": "IC_kwDOBm6k_c5Q3GHJ", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-12-18T01:31:10Z", "updated_at": "2022-12-18T01:31:10Z", "author_association": "OWNER", "body": "During the polling loop it constantly raises:\r\n\r\n`httpx.RemoteProtocolError`: Server disconnected without sending a response", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1496652622, "label": "invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1955#issuecomment-1356625556", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1955", "id": 1356625556, "node_id": "IC_kwDOBm6k_c5Q3HaU", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-12-18T02:00:18Z", "updated_at": "2022-12-18T02:00:18Z", "author_association": "OWNER", "body": "Maybe the reason the ASGI lifespan stuff broke was this line: https://github.com/simonw/datasette/blob/8b73fc6b47dffd8836f5c58aae1e57c1f66a5754/datasette/cli.py#L630-L632", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1496652622, "label": "invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1955#issuecomment-1356625642", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1955", "id": 1356625642, "node_id": "IC_kwDOBm6k_c5Q3Hbq", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-12-18T02:00:57Z", "updated_at": "2022-12-18T02:00:57Z", "author_association": "OWNER", "body": "I added the TLS support here:\r\n- https://github.com/simonw/datasette/issues/1221", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1496652622, "label": "invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1955#issuecomment-1356626334", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1955", "id": 1356626334, "node_id": "IC_kwDOBm6k_c5Q3Hme", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-12-18T02:04:01Z", "updated_at": "2022-12-18T02:04:07Z", "author_association": "OWNER", "body": "I used the steps to test manually from this comment: https://github.com/simonw/datasette/issues/1221#issuecomment-777901052\r\n\r\nIn one terminal:\r\n```\r\ncd /tmp\r\npython -m trustme\r\ndatasette --memory --ssl-keyfile=/tmp/server.key --ssl-certfile=/tmp/server.pem -p 8003\r\n```\r\nThen in another terminal:\r\n```\r\ncurl --cacert /tmp/client.pem 'https://localhost:8003/_memory.json'\r\n```\r\nThis worked correctly, outputting the expected JSON.\r\n\r\nSo the feature still works, it's just the test that is broken for some reason.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1496652622, "label": "invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1955#issuecomment-1356627331", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1955", "id": 1356627331, "node_id": "IC_kwDOBm6k_c5Q3H2D", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-12-18T02:11:17Z", "updated_at": "2022-12-18T02:11:17Z", "author_association": "OWNER", "body": "This issue might be relevant, but I tried the suggested fix in there (`Connection: close` on the incoming requests) and it didn't fix my problem:\r\n- https://github.com/encode/httpx/discussions/2056", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1496652622, "label": "invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1955#issuecomment-1356627931", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1955", "id": 1356627931, "node_id": "IC_kwDOBm6k_c5Q3H_b", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-12-18T02:13:01Z", "updated_at": "2022-12-18T02:13:01Z", "author_association": "OWNER", "body": "Rather than continue to bang my head against this, I'm tempted to rewrite this test to happen outside of Python world - in a bash script run by GitHub Actions, for example.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1496652622, "label": "invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1955#issuecomment-1356629783", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1955", "id": 1356629783, "node_id": "IC_kwDOBm6k_c5Q3IcX", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-12-18T02:18:43Z", "updated_at": "2022-12-18T02:18:43Z", "author_association": "OWNER", "body": "Various attempts at a fix which didn't work:\r\n\r\n```diff\r\ndiff --git a/tests/conftest.py b/tests/conftest.py\r\nindex 69dee68b..899d36fd 100644\r\n--- a/tests/conftest.py\r\n+++ b/tests/conftest.py\r\n@@ -1,4 +1,3 @@\r\n-import asyncio\r\n import httpx\r\n import os\r\n import pathlib\r\n@@ -6,6 +5,7 @@ import pytest\r\n import pytest_asyncio\r\n import re\r\n import subprocess\r\n+import sys\r\n import tempfile\r\n import time\r\n import trustme\r\n@@ -27,13 +27,23 @@ UNDOCUMENTED_PERMISSIONS = {\r\n _ds_client = None\r\n \r\n \r\n-def wait_until_responds(url, timeout=5.0, client=httpx, **kwargs):\r\n+def wait_until_responds(url, timeout=5.0, client=None, **kwargs):\r\n+ client = client or httpx.Client(**kwargs)\r\n start = time.time()\r\n while time.time() - start < timeout:\r\n try:\r\n- client.get(url, **kwargs)\r\n+ if \"verify\" in kwargs:\r\n+ print(kwargs[\"verify\"])\r\n+ print(\r\n+ \"Contents of verify file: {}\".format(\r\n+ open(kwargs.get(\"verify\")).read()\r\n+ )\r\n+ )\r\n+ print(\"client = {}, kwargs = {}\".format(client, kwargs))\r\n+ client.get(url)\r\n return\r\n- except httpx.ConnectError:\r\n+ except (httpx.ConnectError, httpx.RemoteProtocolError) as ex:\r\n+ print(ex)\r\n time.sleep(0.1)\r\n raise AssertionError(\"Timed out waiting for {} to respond\".format(url))\r\n \r\n@@ -166,7 +176,7 @@ def check_permission_actions_are_documented():\r\n @pytest.fixture(scope=\"session\")\r\n def ds_localhost_http_server():\r\n ds_proc = subprocess.Popen(\r\n- [\"datasette\", \"--memory\", \"-p\", \"8041\"],\r\n+ [sys.executable, \"-m\", \"datasette\", \"--memory\", \"-p\", \"8041\"],\r\n stdout=subprocess.PIPE,\r\n stderr=subprocess.STDOUT,\r\n # Avoid FileNotFoundError: [Errno 2] No such file or directory:\r\n@@ -180,7 +190,7 @@ def ds_localhost_http_server():\r\n ds_proc.terminate()\r\n \r\n \r\n-@pytest.fixture(scope=\"session\")\r\n+@pytest.fixture\r\n def ds_localhost_https_server(tmp_path_factory):\r\n cert_directory = tmp_path_factory.mktemp(\"certs\")\r\n ca = trustme.CA()\r\n@@ -194,6 +204,8 @@ def ds_localhost_https_server(tmp_path_factory):\r\n ca.cert_pem.write_to_path(path=client_cert)\r\n ds_proc = subprocess.Popen(\r\n [\r\n+ sys.executable,\r\n+ \"-m\",\r\n \"datasette\",\r\n \"--memory\",\r\n \"-p\",\r\n@@ -207,7 +219,11 @@ def ds_localhost_https_server(tmp_path_factory):\r\n stderr=subprocess.STDOUT,\r\n cwd=tempfile.gettempdir(),\r\n )\r\n- wait_until_responds(\"http://localhost:8042/\", verify=client_cert)\r\n+ wait_until_responds(\r\n+ \"http://localhost:8042/_memory.json\",\r\n+ verify=client_cert,\r\n+ headers={\"Connection\": \"close\"},\r\n+ )\r\n # Check it started successfully\r\n assert not ds_proc.poll(), ds_proc.stdout.read().decode(\"utf-8\")\r\n yield ds_proc, client_cert\r\ndiff --git a/tests/test_cli_serve_server.py b/tests/test_cli_serve_server.py\r\nindex 1c31e2a3..9320b623 100644\r\n--- a/tests/test_cli_serve_server.py\r\n+++ b/tests/test_cli_serve_server.py\r\n@@ -16,7 +16,11 @@ def test_serve_localhost_http(ds_localhost_http_server):\r\n @pytest.mark.serial\r\n def test_serve_localhost_https(ds_localhost_https_server):\r\n _, client_cert = ds_localhost_https_server\r\n- response = httpx.get(\"https://localhost:8042/_memory.json\", verify=client_cert)\r\n+ response = httpx.get(\r\n+ \"https://localhost:8042/_memory.json\",\r\n+ verify=client_cert,\r\n+ headers={\"Connection\": \"close\"},\r\n+ )\r\n assert {\r\n \"database\": \"_memory\",\r\n \"path\": \"/_memory\",\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1496652622, "label": "invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1955#issuecomment-1356630092", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1955", "id": 1356630092, "node_id": "IC_kwDOBm6k_c5Q3IhM", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-12-18T02:20:01Z", "updated_at": "2022-12-18T02:20:01Z", "author_association": "OWNER", "body": "Asked ChatGPT:\r\n\r\n> Write a bash script which starts a server in the background using \"datasette -p 8002\", then uses curl to make a test request against it, then shuts the server down again at the end\r\n\r\nIt gave me:\r\n\r\n```bash\r\n#!/bin/bash\r\n\r\n# Start the server in the background\r\ndatasette -p 8002 &\r\n\r\n# Store the background process ID in a variable\r\nserver_pid=$!\r\n\r\n# Make a test request using curl\r\ncurl http://localhost:8002\r\n\r\n# Shut down the server\r\nkill $server_pid\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1496652622, "label": "invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1955#issuecomment-1356633937", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1955", "id": 1356633937, "node_id": "IC_kwDOBm6k_c5Q3JdR", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-12-18T02:28:22Z", "updated_at": "2022-12-18T02:28:22Z", "author_association": "OWNER", "body": "This `bash` script does the job:\r\n```bash\r\n#!/bin/bash\r\n\r\n# Generate certificates\r\npython -m trustme\r\n# This creates server.pem, server.key, client.pem\r\n\r\n# Start the server in the background\r\ndatasette --memory \\\r\n --ssl-keyfile=server.key \\\r\n --ssl-certfile=server.pem \\\r\n -p 8152 &\r\n\r\n# Store the background process ID in a variable\r\nserver_pid=$!\r\n\r\n# Wait for the server to start\r\nsleep 2\r\n\r\n# Make a test request using curl\r\ncurl -f --cacert client.pem 'https://localhost:8152/_memory.json'\r\n\r\n# Save curl's exit code (-f option causes it to return one on HTTP errors)\r\ncurl_exit_code=$?\r\n\r\n# Shut down the server\r\nkill $server_pid\r\nsleep 1\r\n\r\n# Clean up the certificates\r\nrm server.pem server.key client.pem\r\n\r\necho $curl_exit_code\r\nexit $curl_exit_code\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1496652622, "label": "invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1221#issuecomment-1356639873", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1221", "id": 1356639873, "node_id": "IC_kwDOBm6k_c5Q3K6B", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-12-18T02:39:04Z", "updated_at": "2022-12-18T02:39:04Z", "author_association": "OWNER", "body": "I ended up moving this test out of Python and into a `bash` script here: https://github.com/simonw/datasette/commit/d1d369456a7319b9de39175605568cbc9b852478", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 806849424, "label": "Support SSL/TLS directly"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1955#issuecomment-1356640266", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1955", "id": 1356640266, "node_id": "IC_kwDOBm6k_c5Q3LAK", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-12-18T02:43:00Z", "updated_at": "2022-12-18T02:43:00Z", "author_association": "OWNER", "body": "https://github.com/simonw/datasette/actions/runs/3722908296/jobs/6314093163 shows that new test passing in CI:\r\n\r\n```\r\nGenerated a certificate for 'localhost', '127.0.0.1', '::1'\r\nConfigure your server to use the following files:\r\n cert=/home/runner/work/datasette/datasette/server.pem\r\n key=/home/runner/work/datasette/datasette/server.key\r\nConfigure your client to use the following files:\r\n cert=/home/runner/work/datasette/datasette/client.pem\r\nINFO: Started server process [4036]\r\nINFO: Waiting for application startup.\r\nINFO: Application startup complete.\r\nINFO: Uvicorn running on https://127.0.0.1:8152/ (Press CTRL+C to quit)\r\n % Total % Received % Xferd Average Speed Time Time Time Current\r\n Dload Upload Total Spent Left Speed\r\n\r\nINFO: 127.0.0.1:56726 - \"GET /_memory.json HTTP/1.1\" 200 OK\r\n 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0\r\n100 213 0 213 0 0 11542 0 --:--:-- --:--:-- --:--:-- 11833\r\nINFO: Shutting down\r\nINFO: Waiting for application shutdown.\r\nINFO: Application shutdown complete.\r\nINFO: Finished server process [4036]\r\n{\"database\": \"_memory\", \"private\": false, \"path\": \"/_memory\", \"size\": 0, \"tables\": [], \"hidden_count\": 0, \"views\": [], \"queries\": [], \"allow_execute_sql\": true, \"table_columns\": {}, \"query_ms\": 1.4545189999921604}0\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1496652622, "label": "invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1955#issuecomment-1356640463", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1955", "id": 1356640463, "node_id": "IC_kwDOBm6k_c5Q3LDP", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-12-18T02:45:18Z", "updated_at": "2022-12-18T02:45:18Z", "author_association": "OWNER", "body": "... and with this change, the following now works correctly:\r\n\r\n```\r\n% datasette install datasette-gunicorn\r\n% datasette gunicorn fixtures.db -p 8855\r\n[2022-12-17 18:44:29 -0800] [7651] [INFO] Starting gunicorn 20.1.0\r\n[2022-12-17 18:44:29 -0800] [7651] [INFO] Listening at: http://127.0.0.1:8855 (7651)\r\n[2022-12-17 18:44:29 -0800] [7651] [INFO] Using worker: uvicorn.workers.UvicornWorker\r\n[2022-12-17 18:44:29 -0800] [7653] [INFO] Booting worker with pid: 7653\r\n[2022-12-17 18:44:29 -0800] [7653] [INFO] Started server process [7653]\r\n[2022-12-17 18:44:29 -0800] [7653] [INFO] Waiting for application startup.\r\n[2022-12-17 18:44:29 -0800] [7653] [INFO] Application startup complete.\r\n```\r\nSo this issue is now fixed!", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1496652622, "label": "invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1963#issuecomment-1356651943", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1963", "id": 1356651943, "node_id": "IC_kwDOBm6k_c5Q3N2n", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-12-18T03:23:03Z", "updated_at": "2022-12-18T03:23:03Z", "author_association": "OWNER", "body": "Oh that's annoying... every step in publish succeeded except the static docs one:\r\n\r\n\"image\"\r\n\r\nhttps://github.com/simonw/datasette/actions/runs/3723015082/jobs/6314292722\r\n\r\nThis means the documentation database used to update the search engine on https://datasette.io/ won't reflect the very latest changelog. I'm OK with that - I'll fix this workflow so that next time I publish a release this will work correctly.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1501713288, "label": "0.63.3 bugfix release"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1963#issuecomment-1356652057", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1963", "id": 1356652057, "node_id": "IC_kwDOBm6k_c5Q3N4Z", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-12-18T03:23:22Z", "updated_at": "2022-12-18T03:23:22Z", "author_association": "OWNER", "body": "https://pypi.org/project/datasette/0.63.3/ is released.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1501713288, "label": "0.63.3 bugfix release"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1771#issuecomment-1356655217", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1771", "id": 1356655217, "node_id": "IC_kwDOBm6k_c5Q3Opx", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-12-18T03:38:16Z", "updated_at": "2022-12-18T03:38:16Z", "author_association": "OWNER", "body": "OK I see what you mean:\r\n\r\nhttps://latest.datasette.io/fixtures/attraction_characteristic\r\n\r\n![Animated GIF of the table page hitting tab a bunch - the cog icon highlights and so does the text input but the two select boxes do not](https://user-images.githubusercontent.com/9599/208280176-1e2de671-fe69-43e8-8d62-bf7aa8f4d36e.gif)\r\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1306984363, "label": "minor a11y: has no visual indicator when tabbed to"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1771#issuecomment-1356657451", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1771", "id": 1356657451, "node_id": "IC_kwDOBm6k_c5Q3PMr", "user": {"value": 1473102, "label": "mustafa0x"}, "created_at": "2022-12-18T04:04:32Z", "updated_at": "2022-12-18T04:04:32Z", "author_association": "NONE", "body": "the problem is:\r\n```\r\n.select-wrapper select:focus {\r\n outline: none;\r\n}\r\n```\r\n\r\nI sometimes add this js:\r\n```\r\nwindow.addEventListener('keydown', function check_tab(e) {\r\n if (e.key === 'Tab') {\r\n document.documentElement.classList.add('user-is-tabbing')\r\n window.removeEventListener('keydown', check_tab)\r\n }\r\n})\r\n```\r\n\r\nand then in the css, using a `html.user-is-tabbing` selector undo any outlines I removed.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1306984363, "label": "minor a11y: has no visual indicator when tabbed to"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1771#issuecomment-1356694671", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1771", "id": 1356694671, "node_id": "IC_kwDOBm6k_c5Q3YSP", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-12-18T06:34:20Z", "updated_at": "2022-12-18T06:34:20Z", "author_association": "OWNER", "body": "Now live on https://latest.datasette.io/fixtures/attraction_characteristic", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1306984363, "label": "minor a11y: