skip to content

In REST Assured, auth().form(...) throws "Failed to parse login page" — how do you diagnose and fix it?

level: seniorimportance: must knowfreq 38%

answer

  1. form login is a filter, not a header
  2. an extra GET happens before the POST
  3. requiresParsingOfLoginPage decides
  4. first form, type=text, type=password
  5. three-arg FormAuthConfig skips the parse

basics

~20 s

REST Assured parses the login page only when FormAuthConfig is incomplete. Its auto-detection needs one form element, one text input and one password input, so a scripted or busy page fails. Pass the action and both field names explicitly.

solid answer

~50 s

`auth().form(...)` is not a header scheme: REST Assured replaces the `FormAuthScheme` with an internal `FormAuthFilter` that logs in before your real request. That filter fetches and parses a login page whenever `FormAuthConfig.requiresParsingOfLoginPage()` is true — whenever the form action, the username input name or the password input name is blank, which the two-argument `form(user, password)` always is. The parse runs `XmlPath` in HTML compatibility mode and takes the `action` of the *first* `<form>` element, the name of an `<input type="text">` and the name of an `<input type="password">`. A JavaScript-rendered login screen, a catalogue search box that adds a second text input, or a login form that is not the first form all defeat that, and the filter rethrows the failure as `IllegalArgumentException: Failed to parse login page`. The fix is to name all three yourself — `new FormAuthConfig("/catalogue/perform_login", "staff_id", "staff_pin")` — or use `FormAuthConfig.springSecurity()` when those defaults match.

code

java · 24 lines
java
import io.restassured.authentication.FormAuthConfig;
import org.junit.jupiter.api.Test;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.equalTo;

public class CatalogueFormLoginTest {

    @Test
    void staffCanReadTheHoldsQueue() {
        FormAuthConfig catalogueLogin =
                new FormAuthConfig("/catalogue/perform_login", "staff_id", "staff_pin")
                        .withLoggingEnabled();

        given().
                baseUri("http://localhost:8080").
                auth().form("clerk", "opensesame", catalogueLogin).
        when().
                get("/catalogue/holds").
        then().
                statusCode(200).
                body("branch", equalTo("Riverside"));
    }
}

go deeper

for a junior

Be ready to say that auth().form() makes REST Assured fetch the login page first, and that passing a FormAuthConfig carrying the action and the two input names removes that fetch entirely.

for a middle

Explain that FormAuthScheme is swapped for an internal FormAuthFilter, and that requiresParsingOfLoginPage() is true whenever any of the three values is blank or additional input fields were named.

for a senior

Show how you localise the failure: enable FormAuthConfig.withLoggingEnabled() to see the hidden login exchange, read the real login HTML, then pin the action and both field names rather than trusting detection.

for a principal

Own the policy call of whether suites should authenticate through the login form at all, versus establishing a session at the harness boundary, and what each choice costs in fragility across environments.

## Form authentication is a filter, not a header Most REST Assured authentication ends up as an `AuthenticationScheme` that decorates the outgoing request. Form authentication does not. `FormAuthScheme.authenticate(...)` is an empty method; when `RequestSpecificationImpl` assembles the request and sees a `FormAuthScheme`, it removes every other `AuthFilter` from the filter chain and inserts an internal `FormAuthFilter` at position 0. That filter runs **before** your request: it works out where the login form posts to, performs a login `POST`, copies the cookies from the login response onto your request specification, and only then calls `ctx.next(...)`. Everything that can go wrong with form login goes wrong inside that filter, which is why the failure surfaces as an `IllegalArgumentException` rather than as a matcher mismatch on the response you were actually testing. ## When REST Assured parses the login page at all The filter fetches and parses HTML only when it has to, and one method on the config decides: - `FormAuthConfig.requiresParsingOfLoginPage()` returns `true` when the form action is blank, **or** the username input name is blank, **or** the password input name is blank, **or** the config names additional input fields. - `given().auth().form("clerk", "opensesame")` supplies no config at all, so all three are blank and the parse always happens. - `new FormAuthConfig("/catalogue/perform_login", "staff_id", "staff_pin")` fills all three, so the filter skips straight to the login `POST`. - `withAdditionalField("branch")` puts the parse back, because REST Assured has to read that field's `value` out of the page. - Enabling CSRF also puts it back unconditionally: if `CsrfConfig.isCsrfEnabled()` is true the filter fetches a page whatever the config says, because it needs a token as well as field names. Where the page comes from differs too. With CSRF off, the filter re-sends **your own request** stripped of authentication — `ctx.send(given().spec(requestSpec).auth().none())` — on the assumption that an unauthenticated call to a protected catalogue path returns the login screen; if that comes back `302` it reads the `Location` header and issues the follow-up `GET` itself. With CSRF on, it issues an explicit `GET` of `CsrfConfig.csrfTokenPath` instead. ## What the auto-detection actually greps The body is handed to `new XmlPath(HTML, body)` and three Groovy GPath expressions run over the parsed document: | What it needs | How it is found | What breaks it | |---|---|---| | form action | the `action` attribute of the **first** `<form>` in document order | no `<form>` at all, or the login form is not the first one | | username field | the name of an `<input>` whose `type` is `text` | zero text inputs, or more than one | | password field | the name of an `<input>` whose `type` is `password` | a non-standard input type, or a field added by script | If the action does not start with `/`, REST Assured prepends one, and the login URL is then rebuilt from the scheme, host and port of the request under test — so a form that posts to an entirely different host is not something detection can express. ## Why real login pages defeat it 1. **The page is rendered client-side.** REST Assured is an HTTP client, not a browser; it never runs JavaScript. A login screen whose form is injected by a framework parses to a document with no `<form>` element, `get(0)` fails, and you get the exception. 2. **There is more than one text input.** A catalogue login page that also carries a header search box gives detection two candidates for the username field, and the collected value no longer identifies a single input. 3. **The login form is not the first form.** A newsletter or language-picker form earlier in the markup wins, and REST Assured posts credentials at the wrong action. 4. **The unauthenticated request did not return the login page.** If the service answers with a JSON `401` envelope instead of HTML, there is nothing form-shaped to parse. 5. **A redirect chain hides it.** The filter follows only one explicit `Location` hop for a `302`; a multi-step redirect to an identity page leaves it holding the wrong body. ## Diagnosing it Work from the wire outwards rather than from the credentials inwards: - Turn on `FormAuthConfig.withLoggingEnabled()` (or `withLoggingEnabled(LogDetail.BODY)`) — it logs the login request **and** its response, which is the only view you get of a filter-internal call. - Fetch the login page by hand and read the real markup: count `<form>` elements, count inputs typed `text`, and note the exact `action`. - Check whether the failing exception is `IllegalArgumentException` from the filter or an ordinary status-code assertion. The first means detection; the second means detection succeeded and the credentials or the session did not. - Confirm nothing else on the chain is stripping cookies, since the filter's whole output is `requestSpec.cookies(loggedInResponse.cookies())`. ## The fix Stop letting REST Assured guess. Supply the three values you just read off the page: ```java given().auth().form("clerk", "opensesame", new FormAuthConfig("/catalogue/perform_login", "staff_id", "staff_pin")); ``` `FormAuthConfig.springSecurity()` is the same thing pre-filled with `/j_spring_security_check`, `j_username` and `j_password` — use it only when those really are your form's names. If the page also carries a hidden field the server insists on, add it with `withAdditionalField(...)` and accept the extra round trip that comes back with it. And if the login screen genuinely cannot be driven as HTML, form authentication is the wrong tool for that suite: authenticate some other way and hand the resulting session to the request instead.

  • Where does REST Assured get the login page from when CSRF is not enabled?
    It replays your own request with `auth().none()` through `ctx.send(...)`, expecting an unauthenticated call to a protected catalogue path to return the login screen. If that answers `302` the filter reads the `Location` header and issues the follow-up `GET` itself, because a `302` on a non-`GET` request is not followed automatically.
  • Does supplying a complete FormAuthConfig always remove the extra request?
    No. Two things bring it back. `withAdditionalField(...)` and `withAdditionalFields(...)` have no values to send, so REST Assured must read them out of the HTML. And enabling CSRF forces the fetch regardless, because the filter has to pull a token out of a page before it can post credentials.

Auto-detection is like giving directions as "the house with the red door" — fine until the neighbours repaint or add a porch. Naming the street and number is what FormAuthConfig does.

saying these in an interview costs you the question

  • Thinks auth().form() simply sets an Authorization header
  • Assumes REST Assured runs JavaScript on the login page
  • Blames the credentials when the HTML parse is what failed
  • Believes FormAuthConfig.springSecurity() fits any login form
  • Thinks a complete FormAuthConfig also skips the CSRF token fetch