diff --git a/fasthtml/core.py b/fasthtml/core.py
index 2bffa593..291b016d 100644
--- a/fasthtml/core.py
+++ b/fasthtml/core.py
@@ -145,14 +145,14 @@ def form2dict(form: FormData) -> dict:
async def parse_form(req: Request) -> FormData:
"Starlette errors on empty multipart forms, so this checks for that situation"
ctype = req.headers.get("Content-Type", "")
- if ctype=='application/json': return await req.json()
- if not ctype.startswith("multipart/form-data"): return await req.form()
- try: boundary = ctype.split("boundary=")[1].strip()
- except IndexError: raise HTTPException(400, "Invalid form-data: no boundary")
- min_len = len(boundary) + 6
- clen = int(req.headers.get("Content-Length", "0"))
- if clen <= min_len: return FormData()
- return await req.form()
+ if ctype.startswith("multipart/form-data"):
+ try: boundary = ctype.split("boundary=")[1].strip()
+ except IndexError: raise HTTPException(400, "Invalid form-data: no boundary")
+ if int(req.headers.get("Content-Length", "0")) <= len(boundary) + 6: return FormData()
+ return await req.form()
+ await req.body() # Cache body for non-multipart request types
+ return await req.json() if ctype == 'application/json' else await req.form()
+
# %% ../nbs/api/00_core.ipynb #089fe388
async def _from_body(conn, p, data):
@@ -368,7 +368,7 @@ def _to_xml(req, resp, indent):
"Convert response to XML string with target URL resolution"
resp = _apply_ft(resp)
_find_targets(req, resp)
- return to_xml(resp, indent)
+ return to_xml(resp, indent=indent)
# %% ../nbs/api/00_core.ipynb #f1e3ed2d
_iter_typs = (tuple,list,map,filter,range,types.GeneratorType)
diff --git a/nbs/api/00_core.ipynb b/nbs/api/00_core.ipynb
index 8d7e40ae..ef6657ca 100644
--- a/nbs/api/00_core.ipynb
+++ b/nbs/api/00_core.ipynb
@@ -131,7 +131,7 @@
{
"data": {
"text/plain": [
- "datetime.datetime(2026, 2, 21, 14, 0)"
+ "datetime.datetime(2026, 3, 2, 14, 0)"
]
},
"execution_count": null,
@@ -244,7 +244,7 @@
" 'client': ('127.0.0.1', 8000),\n",
" 'server': ('127.0.0.1', 8000),\n",
" }\n",
- " receive = lambda: {\"body\": b\"\", \"more_body\": False}\n",
+ " async def receive(): return {\"type\": \"http.request\", \"body\": b\"\", \"more_body\": False}\n",
" return Request(scope, receive)"
]
},
@@ -573,14 +573,13 @@
"async def parse_form(req: Request) -> FormData:\n",
" \"Starlette errors on empty multipart forms, so this checks for that situation\"\n",
" ctype = req.headers.get(\"Content-Type\", \"\")\n",
- " if ctype=='application/json': return await req.json()\n",
- " if not ctype.startswith(\"multipart/form-data\"): return await req.form()\n",
- " try: boundary = ctype.split(\"boundary=\")[1].strip()\n",
- " except IndexError: raise HTTPException(400, \"Invalid form-data: no boundary\")\n",
- " min_len = len(boundary) + 6\n",
- " clen = int(req.headers.get(\"Content-Length\", \"0\"))\n",
- " if clen <= min_len: return FormData()\n",
- " return await req.form()"
+ " if ctype.startswith(\"multipart/form-data\"):\n",
+ " try: boundary = ctype.split(\"boundary=\")[1].strip()\n",
+ " except IndexError: raise HTTPException(400, \"Invalid form-data: no boundary\")\n",
+ " if int(req.headers.get(\"Content-Length\", \"0\")) <= len(boundary) + 6: return FormData()\n",
+ " return await req.form()\n",
+ " await req.body() # Cache body for non-multipart request types\n",
+ " return await req.json() if ctype == 'application/json' else await req.form()\n"
]
},
{
@@ -834,6 +833,34 @@
" return await _find_ps(req, data, req.headers, params)"
]
},
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "26df5218",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "ok body=19\n"
+ ]
+ }
+ ],
+ "source": [
+ "def g(test_val:int=0, item:HttpHeader=None, htmx:HtmxHeaders=None, api:ApiReturn=None): ...\n",
+ "\n",
+ "async def f(req):\n",
+ " await _wrap_req(req, _params(g))\n",
+ " body = await req.body()\n",
+ " return Response(f\"ok body={len(body)}\")\n",
+ "\n",
+ "client = TestClient(Starlette(routes=[Route('/', f, methods=['POST'])]))\n",
+ "r = client.post('/', data={'test_val': '21', 'k': 'a', 'v': 'b'}, headers={'HX-Request': '1'})\n",
+ "test_eq(r.status_code, 200)\n",
+ "print(r.text)"
+ ]
+ },
{
"cell_type": "code",
"execution_count": null,
@@ -1238,7 +1265,7 @@
" \"Convert response to XML string with target URL resolution\"\n",
" resp = _apply_ft(resp)\n",
" _find_targets(req, resp)\n",
- " return to_xml(resp, indent)"
+ " return to_xml(resp, indent=indent)"
]
},
{
@@ -2226,13 +2253,7 @@
"name": "stderr",
"output_type": "stream",
"text": [
- "/var/folders/51/b2_szf2945n072c0vj2cyty40000gn/T/ipymini_43537/1456865336.py:30: UserWarning: `self has no type annotation and is not a recognised special name, so is ignored.\n"
- ]
- },
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
+ "/var/folders/51/b2_szf2945n072c0vj2cyty40000gn/T/ipymini_43537/1456865336.py:30: UserWarning: `self has no type annotation and is not a recognised special name, so is ignored.\n",
" if arg!='resp': warn(f\"`{arg} has no type annotation and is not a recognised special name, so is ignored.\")\n"
]
},
@@ -2295,34 +2316,10 @@
"name": "stdout",
"output_type": "stream",
"text": [
- " \n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- " \n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "
Text.
\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- " \n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
+ " \n",
+ " \n",
+ " Text.
\n",
+ " \n",
"\n"
]
}
@@ -2343,48 +2340,12 @@
"name": "stdout",
"output_type": "stream",
"text": [
- " \n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- " \n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- " \n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- " Text.
\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- " \n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- " \n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
+ " \n",
+ " \n",
+ " \n",
+ " Text.
\n",
+ " \n",
+ " \n",
"\n"
]
}
@@ -2405,34 +2366,10 @@
"name": "stdout",
"output_type": "stream",
"text": [
- " \n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- " \n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- " Text.
\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- " \n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
+ " \n",
+ " \n",
+ " Text.
\n",
+ " \n",
"\n"
]
}
@@ -2476,34 +2413,10 @@
"name": "stdout",
"output_type": "stream",
"text": [
- " \n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- " \n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- " Text.
\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- " \n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
+ " \n",
+ " \n",
+ " Text.
\n",
+ " \n",
"\n"
]
}
@@ -2999,13 +2912,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "200\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
+ "200\n",
"content1,content2\n"
]
}
@@ -3460,13 +3367,7 @@
"name": "stderr",
"output_type": "stream",
"text": [
- "/var/folders/51/b2_szf2945n072c0vj2cyty40000gn/T/ipymini_43537/1456865336.py:30: UserWarning: `self has no type annotation and is not a recognised special name, so is ignored.\n"
- ]
- },
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
+ "/var/folders/51/b2_szf2945n072c0vj2cyty40000gn/T/ipymini_43537/1456865336.py:30: UserWarning: `self has no type annotation and is not a recognised special name, so is ignored.\n",
" if arg!='resp': warn(f\"`{arg} has no type annotation and is not a recognised special name, so is ignored.\")\n"
]
}
@@ -3566,13 +3467,7 @@
"name": "stderr",
"output_type": "stream",
"text": [
- "/var/folders/51/b2_szf2945n072c0vj2cyty40000gn/T/ipymini_43537/1456865336.py:30: UserWarning: `self has no type annotation and is not a recognised special name, so is ignored.\n"
- ]
- },
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
+ "/var/folders/51/b2_szf2945n072c0vj2cyty40000gn/T/ipymini_43537/1456865336.py:30: UserWarning: `self has no type annotation and is not a recognised special name, so is ignored.\n",
" if arg!='resp': warn(f\"`{arg} has no type annotation and is not a recognised special name, so is ignored.\")\n"
]
}
@@ -3671,76 +3566,16 @@
"name": "stdout",
"output_type": "stream",
"text": [
- " \n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- " \n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- " \n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- " FastHTML page\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- " \n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- " \n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- " \n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- " \n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- " \n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- " \n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
+ " \n",
+ " \n",
+ " \n",
+ " FastHTML page\n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
"\n"
]
},
@@ -3944,13 +3779,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "Starting slow task\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
+ "Starting slow task\n",
"Finished slow task\n"
]
}
@@ -4003,41 +3832,11 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "Sleeping for 0.0s\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Slept for 0.0s\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Sleeping for 0.001s\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Slept for 0.001s\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Sleeping for 0.002s\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
+ "Sleeping for 0.0s\n",
+ "Slept for 0.0s\n",
+ "Sleeping for 0.001s\n",
+ "Slept for 0.001s\n",
+ "Sleeping for 0.002s\n",
"Slept for 0.002s\n"
]
}
@@ -4063,41 +3862,11 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "Sleeping for 0.0s\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Slept for 0.0s\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Sleeping for 0.001s\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Slept for 0.001s\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Sleeping for 0.002s\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
+ "Sleeping for 0.0s\n",
+ "Slept for 0.0s\n",
+ "Sleeping for 0.001s\n",
+ "Slept for 0.001s\n",
+ "Sleeping for 0.002s\n",
"Slept for 0.002s\n"
]
}
@@ -4122,41 +3891,11 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "Sleeping for 0.0s\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Slept for 0.0s\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Sleeping for 0.001s\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Slept for 0.001s\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Sleeping for 0.002s\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
+ "Sleeping for 0.0s\n",
+ "Slept for 0.0s\n",
+ "Sleeping for 0.001s\n",
+ "Slept for 0.001s\n",
+ "Sleeping for 0.002s\n",
"Slept for 0.002s\n"
]
},
diff --git a/nbs/ref/defining_xt_component.ipynb b/nbs/ref/defining_xt_component.ipynb
index b5bdb42b..eb9c74dd 100644
--- a/nbs/ref/defining_xt_component.ipynb
+++ b/nbs/ref/defining_xt_component.ipynb
@@ -343,7 +343,7 @@
],
"source": [
"div_NotStr = NotStr('I look like a string but render with to_xml
') \n",
- "to_xml(div_NotStr)"
+ "div_NotStr"
]
},
{
@@ -355,13 +355,7 @@
"source": []
}
],
- "metadata": {
- "kernelspec": {
- "display_name": "python3",
- "language": "python",
- "name": "python3"
- }
- },
+ "metadata": {},
"nbformat": 4,
"nbformat_minor": 5
}