Clone on Gitea
Introduction
Explained by ducks
FAQ
Getting Started
Components
Component Basics
Auto Initialization From DOM
Templates
Children and Parents
Roles
Strictness
Fields
What is a Field
Binding Fields to DOM
Reading and Writing Fields
Value Casting
Flags
Events
Overview
Event Handlers
DOM Events
Custom Events
Propagation and Flow
Advanced Event Control
Beyond Components
States
Overview
StateEngine
DisplayStateEngine
Field Matchers
Ordering and priority
Debugging
Advanced usage
Ajax
ComponentUI
VirtualForm
Unit Testing
Helpers
Overview
Cookies
Arrays

Ajax

Ajax is Qite's small HTTP client built on top of fetch(). It exists for the common things you keep rewriting in every app:

  • consistent request/response parsing
  • request/response debug logging
  • defaults and per-request overrides
  • CSRF integration for common backends
  • handler tables for success/error status codes
  • predictable throw rules

The API is intentionally small. You usually call Ajax.get(...) or Ajax.post(...), then await .ready()

Why the name Ajax? It's intentional and it doesn't try to hide what it is. Before fetch, before "data layers", before client abstractions that pretend HTTP does not exist, there was Ajax: a simple, explicit way to send a request and handle a response without reloading the page. Qite does not wrap networking in a new philosophy or invent a magical transport concept. It embraces the boring, honest reality: you are making an HTTP request. The name "Ajax" is a reminder that this layer should stay small, transparent, and unsurprising.

Quick start

A request returns an Ajax instance. To await the request and get the parsed response, call ready(). In Qite, you will usually do this inside a component method and then update fields (or publish an event) based on the result.

Example: shipment tracking lookup.

import Ajax from "/vendor/qite/src/lib/ajax.js";

class TrackingLookupComponent extends BaseComponent {
    constructor(tree_node) {
        super(tree_node);

        this.events.add([
            ["@click", "#lookup", () => this.lookup()]
        ]);
    }

    async lookup() {
        let tracking_id = this.fields.get("tracking_id");

        let resp = await Ajax.get("/shipping/track", {
            tracking_id: tracking_id
        }).ready();

        if (!resp.ok) {
            this.fields.set({ error: "Lookup failed" });
            return;
        }

        this.fields.set({
            status: resp.data.status,
            eta:    resp.data.eta
        });
    }
}

resp is a plain object stored as ajax.resp internally, returned for convenience by ready(). If you do need the Ajax instance (for request/response snapshots or handler tables), keep it:

let ajax = Ajax.get("/shipping/track", { tracking_id: tracking_id });
let resp = await ajax.ready();
console.log(ajax.req, ajax.resp);

What you get back

When the request finishes, ajax.resp is populated with these fields:

  • ok— boolean, same meaning as Response.ok
  • status, status_text, url
  • headers— lowercased header map
  • content_type— convenience alias for headers["content-type"]
  • data— parsed response payload (based on response_type)
  • raw— raw response string for text/html modes, otherwise null
  • id— request id (useful in logs)

The request side is tracked in ajax.req(method, url, headers, body), mostly for debugging.

Request methods

Ajax exposes standard helpers:

Ajax.get(url, data, opts)
Ajax.post(url, data, opts)
Ajax.put(url, data, opts)
Ajax.patch(url, data, opts)
Ajax.delete(url, data, opts)

For GET and HEAD requests, data is encoded into the query string and existing query params are preserved. For all other methods, data becomes the request body (encoded based on request_type).

// GET appends query params
await Ajax.get("/invoices?sort=date", { page: 2 }).ready();

// POST sends a body
await Ajax.post("/invoices", { amount_cents: 1200 }).ready();

Global defaults and per-request overrides

Ajax.configure(...) sets defaults used by all future requests. You can still override any option per request by passing opts to get/post/etc.

Ajax.configure({
    headers: { "X-Requested-With": "XMLHttpRequest" },
    request_type: "json",
    response_type: "json"
});

// Override for one request
await Ajax.get("/report.csv", null, {
    response_type: "text"
}).ready();

Configuration merges:

  • headers are merged deeply (defaults plus overrides)
  • handler tables are merged deeply (defaults plus overrides)
  • debug and throw_on become arrays (single values are wrapped)

Request types

request_type controls how the request body is encoded and which Content-Type is sent. For convenience and to make things concise, Ajax offers built-in request aliases:

  • "json"— JSON.stringify(data), Content-Type application/json
  • "form"— URL-encoded body (a=1&b=2), Content-Type application/x-www-form-urlencoded
  • "multipart"— FormData (no explicit Content-Type, browser sets boundary)
  • "text"— String(data), Content-Type text/plain
  • "html"— String(data), Content-Type text/html
// URL-encoded form body
await Ajax.post("/session", {
    email: "a@b.com",
    pass:  "secret"
}, {
    request_type: "form"
}).ready();

However, you can still use fully-qualified content types:

request_type: "application/x-www-form-urlencoded"

Arrays are supported for form and query strings by repeating keys:

// -> tag=a&tag=b
await Ajax.get("/search", { tag: ["a", "b"] }).ready();

Response types

response_type controls how the response body is parsed and which Accept header is set. Ajax offers the same aliases as for request type : "json", "html" and "text". Example:

let resp = await Ajax.get("/health", null, {
    response_type: "text",
    throw_on: []
}).ready();

console.log(resp.data); // "ok"
console.log(resp.raw);  // "ok"

Content-Type mismatch checks

When response_type is "json", Ajax checks that Content-Type looks like JSON. It accepts application/json and vendor types like application/vnd.x+json. If the response Content-Type does not match response_type and throw_on contains "wrong_response_type", an AjaxError is thrown early.

If you disable "wrong_response_type", Ajax will still try to parse based on response_type and may throw a parse error later.

// Expecting json, but response is text/plain.
// This disables mismatch check, but parse will still fail.
await Ajax.get("/plain_text", null, {
    response_type: "json",
    throw_on: []
}).ready();

Content-Type mismatches usually indicate a backend bug, a misconfigured endpoint, or an unexpected redirect. Failing early makes these problems visible immediately instead of letting bad data propagate deeper into your UI logic.

Throw rules

By default, Ajax only throws on:

  • wrong response type (token: "wrong_response_type"
  • selected HTTP status codes (defaults include "404" and "500")

Throwing is controlled by opts.throw_on, which is a list of tokens:

  • exact status: "404", "500"
  • buckets: "4xx", "5xx"
  • special: "non_2xx"
  • special: "wrong_response_type"
// Treat any 4xx as an exception, but do not throw on mismatched content type.
let resp = await Ajax.post("/charge", { amount_cents: 1200 }, {
    throw_on: ["4xx"]
}).ready();

If a throw happens, you get an AjaxError that may include helpful fields like ajax_id, status, url, and response(the parsed resp snapshot).

Throw rules let you define your error philosophy once. In some systems, HTTP errors are part of normal control flow and should be handled by status handlers. In others, they are exceptional and should bubble up. throw_on makes this explicit and consistent across the application.

Handlers: success_handlers and error_handlers

Sometimes you do not want exceptions and, instead, you want central handling. Ajax supports handler tables for both success and error responses:

  • success_handlers for ok responses (2xx/3xx, based on Response.ok
  • error_handlers for non-ok responses (4xx/5xx)

Match order is the following:

  1. exact status key, for example "200" or "404"
  2. bucket key, for example "2xx" or "4xx"
  3. and then there's fallback key "any"

Handlers receive the Ajax instance, so they can read ajax.req and ajax.resp, and they can still throw if they want. Example:

Ajax.configure({
    success_handlers: {
        "2xx": (ajax) => console.log("ok:", ajax.resp.status)
    },
    error_handlers: {
        "401": (ajax) => window.location.href = "/login",
        "4xx": (ajax) => console.warn("client error:", ajax.resp.status, ajax.resp.data),
        "any": (ajax) => console.error("unexpected error:", ajax.resp.status)
    },
    throw_on: [] // let handlers deal with it
});

Handler tables are a clean alternative to sprinkling try/catch everywhere. They are also a good match for UI code, where you often want to translate HTTP results into user-visible notifications or redirects.

CSRF support

Ajax supports CSRF for typical backends by providing a header name and a token getter function. CSRF headers are only applied to body methods (POST/PUT/PATCH/DELETE), never to GET/HEAD. You can configure CSRF manually:

Ajax.configure({
    csrf_header: "X-CSRF-Token",
    csrf_getter: () => Ajax.metaContent("csrf-token")
});

Or use a template name:

Ajax.configure({ csrf: "Rails" });

Supported templates include: Rails, Django, Phoenix, Laravel, Spring, AspNet. If CSRF is enabled but header name or token is missing, an AjaxError is thrown before sending the request to keep failures obvious.

CSRF is intentionally opt-in. Qite cannot guess how your backend works, so you either configure it explicitly or choose a template.

Debug logging

You can enable debug logging with debug: ["request"], debug: ["response"], or both. Ajax logs as collapsed console groups and includes request/response snapshots.

Ajax.configure({
    debug: ["request", "response"]
});

This is especially useful during integration work, because you can see the final URL, headers, body encoding, status, content type, and parsed payload.

204 No Content

For HTTP 204/205, Ajax sets:

  • resp.data = null
  • resp.raw = null

and skips parsing, regardless of response_type.

A fuller example: checkout charge flow

This example combines common features in a real scenario: request/response type, CSRF, handler tables, and throw rules.

Ajax.configure({
    csrf: "Rails",
    request_type: "json",
    response_type: "json",

    error_handlers: {
        "422": (ajax) => console.warn("invalid:", ajax.resp.data),
        "401": (ajax) => window.location.href = "/login"
    },

    // still throw for server errors
    throw_on: ["5xx", "wrong_response_type"]
});

async function chargeInvoice(invoice_id) {
    try {
        let resp = await Ajax.post("/billing/charge", {
            invoice_id: invoice_id
        }).ready();

        if (resp.ok) {
            console.log("charged:", resp.data);
        }
    } catch (err) {
        // 5xx and parse/type issues land here
        console.error(err);
    }
}