From 4de8f1f7c770df177efaab48a68b6344e32ddc43 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sun, 15 Sep 2024 07:01:11 +1000 Subject: [PATCH] fixes #405 --- fasthtml/_modidx.py | 1 + fasthtml/core.py | 24 +++++++++++++++++------- nbs/api/00_core.ipynb | 32 +++++++++++++++++++++++++------- 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/fasthtml/_modidx.py b/fasthtml/_modidx.py index 6fa57e30..dca0ef73 100644 --- a/fasthtml/_modidx.py +++ b/fasthtml/_modidx.py @@ -95,6 +95,7 @@ 'fasthtml.core.flat_xt': ('api/core.html#flat_xt', 'fasthtml/core.py'), 'fasthtml.core.form2dict': ('api/core.html#form2dict', 'fasthtml/core.py'), 'fasthtml.core.get_key': ('api/core.html#get_key', 'fasthtml/core.py'), + 'fasthtml.core.parse_form': ('api/core.html#parse_form', 'fasthtml/core.py'), 'fasthtml.core.reg_re_param': ('api/core.html#reg_re_param', 'fasthtml/core.py'), 'fasthtml.core.serve': ('api/core.html#serve', 'fasthtml/core.py'), 'fasthtml.core.signal_shutdown': ('api/core.html#signal_shutdown', 'fasthtml/core.py'), diff --git a/fasthtml/core.py b/fasthtml/core.py index 63f261d4..fef00191 100644 --- a/fasthtml/core.py +++ b/fasthtml/core.py @@ -5,9 +5,9 @@ # %% auto 0 __all__ = ['empty', 'htmx_hdrs', 'fh_cfg', 'htmx_resps', 'htmxsrc', 'htmxwssrc', 'fhjsscr', 'htmxctsrc', 'surrsrc', 'scopesrc', 'viewport', 'charset', 'all_meths', 'date', 'snake2hyphens', 'HtmxHeaders', 'str2int', 'HttpHeader', - 'HtmxResponseHeaders', 'form2dict', 'flat_xt', 'Beforeware', 'EventStream', 'signal_shutdown', 'WS_RouteX', - 'uri', 'decode_uri', 'flat_tuple', 'Redirect', 'RouteX', 'RouterX', 'get_key', 'FastHTML', 'serve', 'Client', - 'cookie', 'reg_re_param', 'MiddlewareBase', 'FtResponse'] + 'HtmxResponseHeaders', 'form2dict', 'parse_form', 'flat_xt', 'Beforeware', 'EventStream', 'signal_shutdown', + 'WS_RouteX', 'uri', 'decode_uri', 'flat_tuple', 'Redirect', 'RouteX', 'RouterX', 'get_key', 'FastHTML', + 'serve', 'Client', 'cookie', 'reg_re_param', 'MiddlewareBase', 'FtResponse'] # %% ../nbs/api/00_core.ipynb import json,uuid,inspect,types,uvicorn,signal,asyncio,threading @@ -143,13 +143,25 @@ def form2dict(form: FormData) -> dict: "Convert starlette form data to a dict" return {k: _formitem(form, k) for k in form} +# %% ../nbs/api/00_core.ipynb +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 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() + # %% ../nbs/api/00_core.ipynb async def _from_body(req, p): anno = p.annotation # Get the fields and types of type `anno`, if available d = _annotations(anno) if req.headers.get('content-type', None)=='application/json': data = await req.json() - else: data = form2dict(await req.form()) + else: data = form2dict(await parse_form(req)) cargs = {k: _form_arg(k, v, d) for k, v in data.items() if not d or k in d} return anno(**cargs) @@ -181,9 +193,7 @@ async def _find_p(req, arg:str, p:Parameter): if res in (empty,None): res = req.headers.get(snake2hyphens(arg), None) if res in (empty,None): res = req.query_params.getlist(arg) if res==[]: res = None - if res in (empty,None): - frm = await req.form() - res = _formitem(frm, arg) + if res in (empty,None): res = _formitem(await parse_form(req), arg) # Raise 400 error if the param does not include a default if (res in (empty,None)) and p.default is empty: raise HTTPException(400, f"Missing required field: {arg}") # If we have a default, return that if we have no value diff --git a/nbs/api/00_core.ipynb b/nbs/api/00_core.ipynb index 78a2a56b..4e1c6ef2 100644 --- a/nbs/api/00_core.ipynb +++ b/nbs/api/00_core.ipynb @@ -531,6 +531,26 @@ "test_eq(res['b'], 0)" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "42c9cea0", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "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 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()" + ] + }, { "cell_type": "code", "execution_count": null, @@ -544,7 +564,7 @@ " # Get the fields and types of type `anno`, if available\n", " d = _annotations(anno)\n", " if req.headers.get('content-type', None)=='application/json': data = await req.json()\n", - " else: data = form2dict(await req.form())\n", + " else: data = form2dict(await parse_form(req))\n", " cargs = {k: _form_arg(k, v, d) for k, v in data.items() if not d or k in d}\n", " return anno(**cargs)" ] @@ -637,9 +657,7 @@ " if res in (empty,None): res = req.headers.get(snake2hyphens(arg), None)\n", " if res in (empty,None): res = req.query_params.getlist(arg)\n", " if res==[]: res = None\n", - " if res in (empty,None):\n", - " frm = await req.form()\n", - " res = _formitem(frm, arg)\n", + " if res in (empty,None): res = _formitem(await parse_form(req), arg)\n", " # Raise 400 error if the param does not include a default\n", " if (res in (empty,None)) and p.default is empty: raise HTTPException(400, f\"Missing required field: {arg}\")\n", " # If we have a default, return that if we have no value\n", @@ -2103,13 +2121,13 @@ "name": "stdout", "output_type": "stream", "text": [ - "Set to 2024-09-15 06:30:32.885239\n" + "Set to 2024-09-15 07:00:36.005099\n" ] }, { "data": { "text/plain": [ - "'Session time: 2024-09-15 06:30:32.885239'" + "'Session time: 2024-09-15 07:00:36.005099'" ] }, "execution_count": null, @@ -2340,7 +2358,7 @@ { "data": { "text/plain": [ - "'Cookie was set at time 06:30:34.617249'" + "'Cookie was set at time 07:00:36.291612'" ] }, "execution_count": null,