Repository navigation
HTTP keyword with multipart= file uploads, form=, params=, timeout= and fail_on_status_code=#5109Description
Use case
The HTTP keyword is the library's way to make API calls that automatically share cookies and session state with the open browser page — the main reason to use it over RequestsLibrary. But today it only supports url, method, body and headers, which leaves several very common API-testing needs uncovered:
- File uploads (the headline of this issue): there is currently no way at all to POST a file (
multipart/form-data) via theHTTPkeyword. Avatar upload, document upload and import endpoints are extremely common in real suites, and users are forced to fall back to RequestsLibrary — losing exactly the automatic browser-session cookie sharing they came for. - Classic form posts: endpoints backed by HTML forms (login forms, legacy APIs) expect
application/x-www-form-urlencodedbodies. Users must hand-build the encoded string and set theContent-Typeheader themselves. - Query parameters: special characters must be URL-encoded manually when concatenating query strings.
- Timeouts: every other Browser keyword exposes a
timeoutargument;HTTPis an outlier where a hung endpoint can stall a test indefinitely with no keyword-level control. - Fail-fast on error status: asserting
${res.status}works, but a fail-fast option matching Playwright'sfailOnStatusCodesaves a line in every API test and produces a clearer error at the point of failure.
Proposed keyword / arguments
All new arguments are named-only with None/behavior-preserving defaults:
HTTP url method=GET body=None headers=None
... *, multipart=None form=None params=None timeout=None fail_on_status_code=False
multipart— dict of form fields; each value is either a string or a file spec{name, mimeType, path}(orbufferinstead ofpath), mirroring howUpload File By Selectoralready handles files. Sends the request asmultipart/form-data.form— dict, URL-encoded by the library and sent withContent-Type: application/x-www-form-urlencoded.params— dict of query parameters, URL-encoded and merged into the request URL.timeout— Robot Framework time format (e.g.10 s), per-request timeout.fail_on_status_code— whenTrue, the keyword fails if the response status is not in the 200–399 range.
multipart, form and body are mutually exclusive.
*** Test Cases ***
Upload Avatar Via API
&{file}= Create Dictionary name=avatar.png mimeType=image/png path=${CURDIR}/testdata/avatar.png
&{fields}= Create Dictionary description=Profile picture file=${file}
&{res}= HTTP /api/users/me/avatar method=POST multipart=${fields} timeout=10 s
Should Be Equal ${res.status} ${201}
Legacy Form Login
&{creds}= Create Dictionary username=demo password=mode
&{res}= HTTP /login method=POST form=${creds} fail_on_status_code=True
Search With Query Params
&{query}= Create Dictionary q=robot framework page=1
&{res}= HTTP /api/search params=${query}
Should Be Equal ${res.body.results[0].name} robot frameworkPlaywright API
Maps to the request options of APIRequestContext.fetch/post/etc.: multipart, form, params, timeout, failOnStatusCode.
Note: the library's HTTP keyword is implemented as fetch() inside the page (page.evaluate in node/playwright-wrapper/network.ts), not via a real APIRequestContext, so these options are implemented as equivalents, not direct pass-throughs.
Implementation notes
protobuf/playwright.proto: extend theHttpRequestmessage with the new fields (multipart spec, form dict, params dict, timeout, fail flag).node/playwright-wrapper/network.ts(httpRequest): the multipart body must be built node-side — file contents are read from disk in the Node process (large/binary payloads cannot reasonably cross the gRPC/JSON boundary or be constructed as a binaryFormDatainsidepage.evaluatefrom Python-supplied data), then assembled into themultipart/form-databody and handed to the in-pagefetch.form/paramsare simple encoding steps;timeoutviaAbortControlleraround the fetch.Browser/keywords/network.py(http): new named-only arguments, validation of mutually exclusive body arguments, docs.- Stub regeneration (
inv build), atest coverage against the test app including an upload endpoint.
Backwards compatibility
Fully additive: all five arguments are new, named-only, and default to values that reproduce the current behavior exactly (None/False). Existing calls are unaffected.
Activity
aaltat commentedon Nov 7, 2026
Update after the Playwright 1.63–1.65 review (
mainat 5e35099)HTTPstill takes onlyurl,method,bodyandheaders(Browser/keywords/network.py:59-65). It still runsfetch()inside the page throughpage.evaluate(node/playwright-wrapper/network.ts:26-54,fetchat:36). The wrapper does not useAPIRequestContext(context.request) anywhere. The issue's description of the implementation is still correct.addCookies(),cookies()andclearCookies()onAPIRequestContext. Forcontext.request, these act on the same cookie jar as the browser context. The library already reaches that jar through its context cookie keywords, so these methods change nothing here.params. Whenparamsis a string orURLSearchParams, Playwright now appends it to the query the URL already has instead of replacing that query (fix(fetch): append string and URLSearchParams params to the URL query microsoft/playwright#43226). The object form has always appended. So from 1.65 every form of Playwright'sparamsappends, and duplicate keys are kept. The proposedparams=dict should follow the same rule whether it is built on the in-page fetch or oncontext.request.APIRequestContextfixes. Cookies are now selected with RFC 6265 domain and path matching (fix(cookies): follow RFC 6265 domain and path matching in cookies(urls) microsoft/playwright#43148), nameless cookies are handled per RFC 6265bis (fix(fetch): serialize and parse nameless cookies per RFC 6265bis microsoft/playwright#43168), and quotes and newlines in multipart field and file names are escaped (fix(fetch): escape quotes and newlines in multipart field and file names microsoft/playwright#43430). The 1.63 change only added TypeScript generics.context.requestwould give. We would getmultipart,form,params,timeout,failOnStatusCode,maxRedirectsandmaxRetrieswithout building them ourselves; the file-reading and multipart-building code from the implementation notes would not be needed. The keyword would also work without an open page. Requests use the context's cookies,extraHTTPHeaders,httpCredentials, proxy andignoreHTTPSErrors, andSet-Cookiein a response updates the context.urlis resolved against the current page URL today (network.py:69).context.requestresolves it only againstbaseURL, soHTTP /api/xfails in a context withoutbaseURL.context.requestare not page network traffic.Wait For ResponseandWait For Requestwould not see them, andatest/test/05_JS_Tests/http_with_waiting.robotrelies on that pairing in every test.type(basic,cors) andredirected(network.py:89-90) do not exist onAPIResponse.redirectedcould be derived;typecould not.credentials: 'same-origin', so a cross-origin call sends no cookies today.context.requesthas no CORS and sends every matching cookie to every origin. That means fewer failures but a different security posture.bodytocontext.requestas aBuffer, not a string. With a string and anapplication/jsoncontent type, Playwright JSON-encodes the string again when it does not parse as JSON. Since 1.65 that check also matchesapplication/json; charset=...(fix(fetch): json stringify string data when content-type has parameters microsoft/playwright#43395).HTTPas the in-page fetch and add the arguments the issue lists by encoding them in the wrapper, which is what the issue proposes. Choosecontext.requestonly together with an explicit, documented break for relative URLs andWait For Response. Do not switch the backend without saying so.