Skip to content

Extend HTTP keyword with multipart= file uploads, form=, params=, timeout= and fail_on_status_code= #5109

Description

@Snooz82

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 the HTTP keyword. 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-urlencoded bodies. Users must hand-build the encoded string and set the Content-Type header themselves.
  • Query parameters: special characters must be URL-encoded manually when concatenating query strings.
  • Timeouts: every other Browser keyword exposes a timeout argument; HTTP is 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's failOnStatusCode saves 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} (or buffer instead of path), mirroring how Upload File By Selector already handles files. Sends the request as multipart/form-data.
  • form — dict, URL-encoded by the library and sent with Content-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 — when True, 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 framework

Playwright 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 the HttpRequest message 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 binary FormData inside page.evaluate from Python-supplied data), then assembled into the multipart/form-data body and handed to the in-page fetch. form/params are simple encoding steps; timeout via AbortController around 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

  1. self-assigned this
    on Aug 6, 2026
  2. added theissue type on Aug 6, 2026
  3. added this to the Ecosystem project milestone on Sep 29, 2026
  4. removed their assignment
    on Nov 1, 2026
  5. aaltat commented on Nov 7, 2026

    @aaltat
    Member

    Update after the Playwright 1.63–1.65 review (main at 5e35099)

    • Still not implemented. HTTP still takes only url, method, body and headers (Browser/keywords/network.py:59-65). It still runs fetch() inside the page through page.evaluate (node/playwright-wrapper/network.ts:26-54, fetch at :36). The wrapper does not use APIRequestContext (context.request) anywhere. The issue's description of the implementation is still correct.
    • New in 1.64: addCookies(), cookies() and clearCookies() on APIRequestContext. For context.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.
    • 1.65 behaviour change to params. When params is a string or URLSearchParams, 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's params appends, and duplicate keys are kept. The proposed params= dict should follow the same rule whether it is built on the in-page fetch or on context.request.
    • Other 1.65 APIRequestContext fixes. 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.
    • What moving to context.request would give. We would get multipart, form, params, timeout, failOnStatusCode, maxRedirects and maxRetries without 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 and ignoreHTTPSErrors, and Set-Cookie in a response updates the context.
    • What it would change for users. These differences are visible to users and would need a deprecation path or a separate code path:
      • A relative url is resolved against the current page URL today (network.py:69). context.request resolves it only against baseURL, so HTTP /api/x fails in a context without baseURL.
      • Requests made with context.request are not page network traffic. Wait For Response and Wait For Request would not see them, and atest/test/05_JS_Tests/http_with_waiting.robot relies on that pairing in every test.
      • The response keys type (basic, cors) and redirected (network.py:89-90) do not exist on APIResponse. redirected could be derived; type could not.
      • The in-page fetch follows the browser's CORS rules and uses the default credentials: 'same-origin', so a cross-origin call sends no cookies today. context.request has no CORS and sends every matching cookie to every origin. That means fewer failures but a different security posture.
      • Pass body to context.request as a Buffer, not a string. With a string and an application/json content type, Playwright JSON-encodes the string again when it does not parse as JSON. Since 1.65 that check also matches application/json; charset=... (fix(fetch): json stringify string data when content-type has parameters microsoft/playwright#43395).
    • Suggestion. Keep HTTP as the in-page fetch and add the arguments the issue lists by encoding them in the wrapper, which is what the issue proposes. Choose context.request only together with an explicit, documented break for relative URLs and Wait For Response. Do not switch the backend without saying so.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions