← pod_r5_v1h

fastapi_13537

resolved RESOLVED PASS · 16 tool calls · 178 s · fastapi/fastapi

Task input

🐛 Fix support for form values with empty strings interpreted as missing (`None` if that's the default), for compatibility with HTML forms

There was a bug in the validation of form data for optional params with None default value. This is a regression that was introduced by https://github.com/fastapi/fastapi/pull/12134.

## Explaination:
The `_get_multidict_value` uses `None` to represent both the absence a value in the dict, and a default value of `None` for that field. When the values are collected in `_extract_form_body`, they are only added to the values if they are not `None`. Later (#12134), some code was added that attempted to complement the model values with values not explicitely in the model, if values for those fields were not already processed earlier. The issue is that since we didn't add values to the initial value dict if they were none, we attempted to add them again in that second iteration. This generally wasn't an issue, except for the fast that a form field set to the empty string (`""`) is interpreted as the absence of that field. This resulted in `_get_multidict_value` picking the default value, that would happen to be `None`, so it wasn't added to the values dict in the first iteration. In the second iteration, we would find that value in the request params again, but set to `""`, and add that to the initial value dict. Later, validation would choke on that empty string.

The fix is to keep track of already visited field, rather that relying on the content of the values dict to complement the missing field. Such thing was already done in `request_params_to_args` for example.


closes #13533 

Tool calls (17)

#ToolArgumentsResult
1run_command
{
 "command": "grep -rn \"_extract_form_body\" . | head -20"
}
{"status": "ok", "stdout": "./fastapi/dependencies/utils.py:869:async def _extract_form_body(\n./fastapi/dependencies/utils.py:933: body_to_process = await _ext
full
{"status": "ok", "stdout": "./fastapi/dependencies/utils.py:869:async def _extract_form_body(\n./fastapi/dependencies/utils.py:933:        body_to_process = await _extract_form_body(fields_to_extract, received_body)\n", "stderr": "", "exit_code": 0}
2run_command
{
 "command": "grep -n \"_get_multidict_value\" fastapi/dependencies/utils.py"
}
{"status": "ok", "stdout": "723:def _get_multidict_value(\n788: value = _get_multidict_value(field, received_params, alias=alias)\n810: value = _get_multidict_v
full
{"status": "ok", "stdout": "723:def _get_multidict_value(\n788:        value = _get_multidict_value(field, received_params, alias=alias)\n810:        value = _get_multidict_value(field, received_params)\n876:        value = _get_multidict_value(field, received_body)\n", "stderr": "", "exit_code": 0}
3run_command
{
 "command": "sed -n '723,940p' fastapi/dependencies/utils.py"
}
{"status": "ok", "stdout": "def _get_multidict_value(\n field: ModelField, values: Mapping[str, Any], alias: Union[str, None] = None\n) -> Any:\n alias = alias
full
{"status": "ok", "stdout": "def _get_multidict_value(\n    field: ModelField, values: Mapping[str, Any], alias: Union[str, None] = None\n) -> Any:\n    alias = alias or field.alias\n    if is_sequence_field(field) and isinstance(values, (ImmutableMultiDict, Headers)):\n        value = values.getlist(alias)\n    else:\n        value = values.get(alias, None)\n    if (\n        value is None\n        or (\n            isinstance(field.field_info, (params.Form, temp_pydantic_v1_params.Form))\n            and isinstance(value, str)  # For type checks\n            and value == \"\"\n        )\n        or (is_sequence_field(field) and len(value) == 0)\n    ):\n        if field.required:\n            return\n        else:\n            return deepcopy(field.default)\n    return value\n\n\ndef request_params_to_args(\n    fields: Sequence[ModelField],\n    received_params: Union[Mapping[str, Any], QueryParams, Headers],\n) -> Tuple[Dict[str, Any], List[Any]]:\n    values: Dict[str, Any] = {}\n    errors: List[Dict[str, Any]] = []\n\n    if not fields:\n        return values, errors\n\n    first_field = fields[0]\n    fields_to_extract = fields\n    single_not_embedded_field = False\n    default_convert_underscores = True\n    if len(fields) == 1 and lenient_issubclass(first_field.type_, BaseModel):\n        fields_to_extract = get_cached_model_fields(first_field.type_)\n        single_not_embedded_field = True\n        # If headers are in a Pydantic model, the way to disable convert_underscores\n        # would be with Header(convert_underscores=False) at the Pydantic model level\n        default_convert_underscores = getattr(\n            first_field.field_info, \"convert_underscores\", True\n        )\n\n    params_to_process: Dict[str, Any] = {}\n\n    processed_keys = set()\n\n    for field in fields_to_extract:\n        alias = None\n        if isinstance(received_params, Headers):\n            # Handle fields extracted from a Pydantic Model for a header, each field\n  
4run_command
{
 "command": "sed -n '869,940p' fastapi/dependencies/utils.py"
}
{"status": "ok", "stdout": "async def _extract_form_body(\n body_fields: List[ModelField],\n received_body: FormData,\n) -> Dict[str, Any]:\n values = {}\n\n fo
full
{"status": "ok", "stdout": "async def _extract_form_body(\n    body_fields: List[ModelField],\n    received_body: FormData,\n) -> Dict[str, Any]:\n    values = {}\n\n    for field in body_fields:\n        value = _get_multidict_value(field, received_body)\n        field_info = field.field_info\n        if (\n            isinstance(field_info, (params.File, temp_pydantic_v1_params.File))\n            and is_bytes_field(field)\n            and isinstance(value, UploadFile)\n        ):\n            value = await value.read()\n        elif (\n            is_bytes_sequence_field(field)\n            and isinstance(field_info, (params.File, temp_pydantic_v1_params.File))\n            and value_is_sequence(value)\n        ):\n            # For types\n            assert isinstance(value, sequence_types)  # type: ignore[arg-type]\n            results: List[Union[bytes, str]] = []\n\n            async def process_fn(\n                fn: Callable[[], Coroutine[Any, Any, Any]],\n            ) -> None:\n                result = await fn()\n                results.append(result)  # noqa: B023\n\n            async with anyio.create_task_group() as tg:\n                for sub_value in value:\n                    tg.start_soon(process_fn, sub_value.read)\n            value = serialize_sequence_value(field=field, value=results)\n        if value is not None:\n            values[field.alias] = value\n    for key, value in received_body.items():\n        if key not in values:\n            values[key] = value\n    return values\n\n\nasync def request_body_to_args(\n    body_fields: List[ModelField],\n    received_body: Optional[Union[Dict[str, Any], FormData]],\n    embed_body_fields: bool,\n) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]:\n    values: Dict[str, Any] = {}\n    errors: List[Dict[str, Any]] = []\n    assert body_fields, \"request_body_to_args() should be called with fields\"\n    single_not_embedded_field = len(body_fields) == 1 and not embed_body_fields\n    first_fiel
5run_command
{
 "command": "python3 - <<'PY'\nfrom fastapi import FastAPI, Form\nfrom fastapi.testclient import TestClient\nfrom pydantic import BaseModel\nfrom typing import Optional\n\napp = FastAPI()\n\n@app.post(\"/test\")\nasync def test_endpoint(name: Optional[str] = Form(None)):\n    return {\"name\": name}\n\nclient = TestClient(app)\n\ndef test_empty_string_form():\n    # When name=\"\" is sent in form, it should be interpreted as missing (None)\n    # if the default is None.\n    response = client.post(\"/test\", data={\"name\": \"\"})\n    print(f\"Response with name='': {response.json()}\")\n    assert response.json() == {\"name\": None}\n\ntest_empty_string_form()\nPY\n"
}
{"status": "error", "error_type": "CommandError", "error_message": "Traceback (most recent call last):\n File \"<stdin>\", line 21, in <module>\n File \"<stdin>
full
{"status": "error", "error_type": "CommandError", "error_message": "Traceback (most recent call last):\n  File \"<stdin>\", line 21, in <module>\n  File \"<stdin>\", line 19, in test_empty_string_form\nAssertionError\n", "details": {"stdout": "Response with name='': {'name': ''}\n", "stderr": "Traceback (most recent call last):\n  File \"<stdin>\", line 21, in <module>\n  File \"<stdin>\", line 19, in test_empty_string_form\nAssertionError\n", "exit_code": 1}}
6run_command
{
 "command": "python3 - <<'PY'\nfrom fastapi import FastAPI, Form\nfrom fastapi.testclient import TestClient\nfrom typing import Optional\n\napp = FastAPI()\n\n@app.post(\"/test\")\nasync def test_endpoint(name: Optional[str] = Form(\"default\")):\n    return {\"name\": name}\n\nclient = TestClient(app)\n\ndef test_empty_string_form_with_default():\n    # When name=\"\" is sent in form, it should be interpreted as missing (None)\n    # if the default is \"default\".\n    response = client.post(\"/test\", data={\"name\": \"\"})\n    print(f\"Response with empty string and default 'default': {response.json()}\")\n    # Expected: should use default value \"default\"\n    assert response.json() == {\"name\": \"default\"}\n\ntest_empty_string_form_with_default()\nPY\n"
}
{"status": "ok", "stdout": "Response with empty string and default 'default': {'name': 'default'}\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "Response with empty string and default 'default': {'name': 'default'}\n", "stderr": "", "exit_code": 0}
7run_command
{
 "command": "python3 - <<'PY'\nfrom fastapi import FastAPI, Form\nfrom fastapi.testclient import TestClient\nfrom typing import Optional\n\napp = FastAPI()\n\n@app.post(\"/test\")\nasync def test_endpoint(name: Optional[str] = Form(None)):\n    return {\"name\": name}\n\nclient = TestClient(app)\n\ndef test_empty_string_form():\n    # This is the case described in the issue.\n    # name=\"\" is sent. \n    # _get_multidict_value(field, received_body) sees name=\"\", and since default is None, it returns None.\n    # In _extract_form_body:\n    # if value is not None: values[field.alias] = value  <-- value is None, so not added.\n    # Then:\n    # for key, value in received_body.items():\n    #     if key not in values:\n    #         values[key] = value  <-- 'name' is not in values, so it's added as \"\".\n    # Final values = {\"name\": \"\"}\n    # Validation then uses \"\", and since it's Optional[str], it stays \"\".\n    \n    response = client.post(\"/test\", data={\"name\": \"\"})\n    print(f\"Response with name='': {response.json()}\")\n    # If the bug is present, it returns {'name': ''}.\n    # If fixed, it should return {'name': None} because the empty string should be treated as missing.\n    assert response.json() == {\"name\": None}\n\ntest_empty_string_form()\nPY\n"
}
{"status": "error", "error_type": "CommandError", "error_message": "Traceback (most recent call last):\n File \"<stdin>\", line 32, in <module>\n File \"<stdin>
full
{"status": "error", "error_type": "CommandError", "error_message": "Traceback (most recent call last):\n  File \"<stdin>\", line 32, in <module>\n  File \"<stdin>\", line 30, in test_empty_string_form\nAssertionError\n", "details": {"stdout": "Response with name='': {'name': ''}\n", "stderr": "Traceback (most recent call last):\n  File \"<stdin>\", line 32, in <module>\n  File \"<stdin>\", line 30, in test_empty_string_form\nAssertionError\n", "exit_code": 1}}
8run_command
{
 "command": "python3 - <<'PY'\nfrom fastapi import FastAPI, Form\nfrom fastapi.testclient import TestClient\nfrom typing import Optional\n\napp = FastAPI()\n\n@app.post(\"/test\")\nasync def test_endpoint(name: Optional[int] = Form(None)):\n    return {\"name\": name}\n\nclient = TestClient(app)\n\ndef test_empty_string_form_int():\n    # If the field is int, and empty string is sent, it should be None (default).\n    # But if it's added back as \"\", validation of int will fail.\n    try:\n        response = client.post(\"/test\", data={\"name\": \"\"})\n        print(f\"Response with name='': {response.json()}\")\n    except Exception as e:\n        print(f\"Caught exception: {e}\")\n    \n    # If the bug is present, it will likely return a 422 Unprocessable Entity.\n    # If fixed, it should return {'name': None}.\n    # We use a request and check status code.\n    response = client.post(\"/test\", data={\"name\": \"\"})\n    print(f\"Status code: {response.status_code}, JSON: {response.json()}\")\n    assert response.status_code == 200\n    assert response.json() == {\"name\": None}\n\ntest_empty_string_form_int()\nPY\n"
}
{"status": "error", "error_type": "CommandError", "error_message": "Traceback (most recent call last):\n File \"<stdin>\", line 30, in <module>\n File \"<stdin>
full
{"status": "error", "error_type": "CommandError", "error_message": "Traceback (most recent call last):\n  File \"<stdin>\", line 30, in <module>\n  File \"<stdin>\", line 27, in test_empty_string_form_int\nAssertionError\n", "details": {"stdout": "Response with name='': {'detail': [{'type': 'int_parsing', 'loc': ['body', 'name'], 'msg': 'Input should be a valid integer, unable to parse string as an integer', 'input': ''}]}\nStatus code: 422, JSON: {'detail': [{'type': 'int_parsing', 'loc': ['body', 'name'], 'msg': 'Input should be a valid integer, unable to parse string as an integer', 'input': ''}]}\n", "stderr": "Traceback (most recent call last):\n  File \"<stdin>\", line 30, in <module>\n  File \"<stdin>\", line 27, in test_empty_string_form_int\nAssertionError\n", "exit_code": 1}}
9run_command
{
 "command": "python3 - <<'PY'\nfrom fastapi import FastAPI, Form\nfrom fastapi.testclient import TestClient\nfrom typing import Optional\n\napp = FastAPI()\n\n@app.post(\"/test\")\nasync def test_endpoint(name: Optional[str] = Form(None)):\n    return {\"name\": name}\n\nclient = TestClient(app)\n\ndef test_empty_string_form():\n    response = client.post(\"/test\", data={\"name\": \"\"})\n    print(f\"Response: {response.json()}\")\n    # This test should fail if the bug is present, and pass if fixed.\n    assert response.json() == {\"name\": None}\n\ntest_empty_string_form()\nPY\n"
}
{"status": "error", "error_type": "CommandError", "error_message": "Traceback (most recent call last):\n File \"<stdin>\", line 19, in <module>\n File \"<stdin>
full
{"status": "error", "error_type": "CommandError", "error_message": "Traceback (most recent call last):\n  File \"<stdin>\", line 19, in <module>\n  File \"<stdin>\", line 17, in test_empty_string_form\nAssertionError\n", "details": {"stdout": "Response: {'name': ''}\n", "stderr": "Traceback (most recent call last):\n  File \"<stdin>\", line 19, in <module>\n  File \"<stdin>\", line 17, in test_empty_string_form\nAssertionError\n", "exit_code": 1}}
10run_command
{
 "command": "cd /workspace && python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/dependencies/utils.py\")\ns = p.read_text()\n\n# Current _extract_form_body implementation:\nold = \"\"\"    for field in body_fields:\n        value = _get_multidict_value(field, received_body)\n        field_info = field.field_info\n        if (\n            isinstance(field_info, (params.File, temp_pydantic_v1_params.File))\n            and is_bytes_field(field)\n            and isinstance(value, UploadFile)\n        ):\n            value = await value.read()\n        elif (\n            is_bytes_sequence_field(field)\n            and isinstance(field_info, (params.File, temp_pydantic_v1_params.File))\n            and value_is_sequence(value)\n        ):\n            # For types\n            assert isinstance(value, sequence_types)  # type: ignore[arg-type]\n            results: List[Union[bytes, str]] = []\n\n            async def process_fn(\n                fn: Callable[[], Coroutine[Any, Any, Any]],\n            ) -> None:\n                result = await fn()\n                results.append(result)  # noqa: B023\n\n            async with anyio.create_task_group() as tg:\n                for sub_value in value:\n                    tg.start_soon(process_fn, sub_value.read)\n            value = serialize_sequence_value(field=field, value=results)\n        if value is not None:\n            values[field.alias] = value\n    for key, value in received_body.items():\n        if key not in values:\n            values[key] = value\"\"\"\n\n# Fixed implementation: keep track of processed keys.\nnew = \"\"\"    processed_keys = set()\n    for field in body_fields:\n        value = _get_multidict_value(field, received_body)\n        field_info = field.field_info\n        if (\n            isinstance(field_info, (params.File, temp_pydantic_v1_params.File))\n            and is_bytes_field(field)\n            and isinstance(value, UploadFile)\n        ):\n            value = await value.read()\n        elif (\n            is_bytes_sequence_field(field)\n            and isinstance(field_info, (params.File, temp_pydantic_v1_params.File))\n            and value_is_sequence(value)\n        ):\n            # For types\n            assert isinstance(value, sequence_types)  # type: ignore[arg-type]\n            results: List[Union[bytes, str]] = []\n\n            async def process_fn(\n                fn: Callable[[], Coroutine[Any, Any, Any]],\n            ) -> None:\n                result = await fn()\n                results.append(result)  # noqa: B023\n\n            async with anyio.create_task_group() as tg:\n                for sub_value in value:\n                    tg.start_soon(process_fn, sub_value.read)\n            value = serialize_sequence_value(field=field, value=results)\n        if value is not None:\n            values[field.alias] = value\n        processed_keys.add(field.alias)\n    for key, value in received_body.items():\n        if key not in processed_keys:\n            values[key] = value\"\"\"\n\nassert s.count(old) == 1, \"old text not found\"\nt = s.replace(old, new)\np.write_text(t)\nPY\n"
}
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
11run_command
{
 "command": "python3 - <<'PY'\nfrom fastapi import FastAPI, Form\nfrom fastapi.testclient import TestClient\nfrom typing import Optional\n\napp = FastAPI()\n\n@app.post(\"/test\")\nasync def test_endpoint(name: Optional[str] = Form(None)):\n    return {\"name\": name}\n\nclient = TestClient(app)\n\ndef test_empty_string_form():\n    response = client.post(\"/test\", data={\"name\": \"\"})\n    print(f\"Response: {response.json()}\")\n    # Now it should be None because empty string is interpreted as missing\n    # and NOT added back in the second loop because 'name' is in processed_keys.\n    assert response.json() == {\"name\": None}\n\ntest_empty_string_form()\nPY\n"
}
{"status": "ok", "stdout": "Response: {'name': None}\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "Response: {'name': None}\n", "stderr": "", "exit_code": 0}
12run_command
{
 "command": "python3 - <<'PY'\nfrom fastapi import FastAPI, Form\nfrom fastapi.testclient import TestClient\nfrom typing import Optional\n\napp = FastAPI()\n\n@app.post(\"/test\")\nasync def test_endpoint(name: Optional[int] = Form(None)):\n    return {\"name\": name}\n\nclient = TestClient(app)\n\ndef test_empty_string_form_int():\n    response = client.post(\"/test\", data={\"name\": \"\"})\n    print(f\"Response: {response.json()}\")\n    # Should be None, not 422.\n    assert response.status_code == 200\n    assert response.json() == {\"name\": None}\n\ntest_empty_string_form_int()\nPY\n"
}
{"status": "ok", "stdout": "Response: {'name': None}\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "Response: {'name': None}\n", "stderr": "", "exit_code": 0}
13run_command
{
 "command": "python3 - <<'PY'\nfrom fastapi import FastAPI, Form\nfrom fastapi.testclient import TestClient\nfrom typing import Optional\n\napp = FastAPI()\n\n@app.post(\"/test\")\nasync def test_endpoint(name: Optional[str] = Form(\"default\")):\n    return {\"name\": name}\n\nclient = TestClient(app)\n\ndef test_empty_string_form_with_default():\n    response = client.post(\"/test\", data={\"name\": \"\"})\n    print(f\"Response: {response.json()}\")\n    # Should be 'default'.\n    assert response.json() == {\"name\": \"default\"}\n\ntest_empty_string_form_with_default()\nPY\n"
}
{"status": "ok", "stdout": "Response: {'name': 'default'}\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "Response: {'name': 'default'}\n", "stderr": "", "exit_code": 0}
14run_command
{
 "command": "python3 - <<'PY'\nfrom fastapi import FastAPI, Form\nfrom fastapi.testclient import TestClient\nfrom typing import Optional\n\napp = FastAPI()\n\n@app.post(\"/test\")\nasync def test_endpoint(name: str = Form(...)):\n    return {\"name\": name}\n\nclient = TestClient(app)\n\ndef test_empty_string_form_required():\n    # If required, empty string should trigger validation error (if it's treated as missing)\n    response = client.post(\"/test\", data={\"name\": \"\"})\n    print(f\"Status: {response.status_code}, JSON: {response.json()}\")\n    assert response.status_code == 422\n\ntest_empty_string_form_required()\nPY\n"
}
{"status": "ok", "stdout": "Status: 422, JSON: {'detail': [{'type': 'missing', 'loc': ['body', 'name'], 'msg': 'Field required', 'input': None}]}\n", "stderr":
full
{"status": "ok", "stdout": "Status: 422, JSON: {'detail': [{'type': 'missing', 'loc': ['body', 'name'], 'msg': 'Field required', 'input': None}]}\n", "stderr": "", "exit_code": 0}
15run_command
{
 "command": "git diff"
}
{"status": "ok", "stdout": "diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py\nindex d43fa8a5..4e952adf 100644\n--- a/fastapi/dependenc
full
{"status": "ok", "stdout": "diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py\nindex d43fa8a5..4e952adf 100644\n--- a/fastapi/dependencies/utils.py\n+++ b/fastapi/dependencies/utils.py\n@@ -872,6 +872,7 @@ async def _extract_form_body(\n ) -> Dict[str, Any]:\n     values = {}\n \n+    processed_keys = set()\n     for field in body_fields:\n         value = _get_multidict_value(field, received_body)\n         field_info = field.field_info\n@@ -902,8 +903,9 @@ async def _extract_form_body(\n             value = serialize_sequence_value(field=field, value=results)\n         if value is not None:\n             values[field.alias] = value\n+        processed_keys.add(field.alias)\n     for key, value in received_body.items():\n-        if key not in values:\n+        if key not in processed_keys:\n             values[key] = value\n     return values\n \n", "stderr": "", "exit_code": 0}
16run_command
{
 "command": "submit_patch"
}
{"status": "error", "error_type": "CommandError", "error_message": "/bin/bash: submit_patch: command not found\n", "details": {"stdout": "", "stderr": "/bin/bas
full
{"status": "error", "error_type": "CommandError", "error_message": "/bin/bash: submit_patch: command not found\n", "details": {"stdout": "", "stderr": "/bin/bash: submit_patch: command not found\n", "exit_code": 127}}
17submit_patch
{}
{"status": "ok", "patch_size": 835, "files_changed": 1}
full
{"status": "ok", "patch_size": 835, "files_changed": 1}

Patch

diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py
index d43fa8a5..4e952adf 100644
--- a/fastapi/dependencies/utils.py
+++ b/fastapi/dependencies/utils.py
@@ -872,6 +872,7 @@ async def _extract_form_body(
 ) -> Dict[str, Any]:
     values = {}
 
+    processed_keys = set()
     for field in body_fields:
         value = _get_multidict_value(field, received_body)
         field_info = field.field_info
@@ -902,8 +903,9 @@ async def _extract_form_body(
             value = serialize_sequence_value(field=field, value=results)
         if value is not None:
             values[field.alias] = value
+        processed_keys.add(field.alias)
     for key, value in received_body.items():
-        if key not in values:
+        if key not in processed_keys:
             values[key] = value
     return values
 

Test output

show
..                                                                       [100%]
=============================== warnings summary ===============================
../../../../../../../Users/jp/repos/kaggle-gemini-coding-agent-post-training/.envs/overlays/starlette-0.50.0-py3-none-any/starlette/testclient.py:45
  /Users/jp/repos/kaggle-gemini-coding-agent-post-training/.envs/overlays/starlette-0.50.0-py3-none-any/starlette/testclient.py:45: DeprecationWarning: The anyio.abc.BlockingPortal alias is deprecated, use anyio.from_thread.BlockingPortal instead.
    _PortalFactoryType = Callable[[], AbstractContextManager[anyio.abc.BlockingPortal]]

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
2 passed, 1 warning in 0.57s