The Request Is a Contract, Not Just a URL
This chapter follows a Hoppscotch request from saved or edited data to an executable network operation and back to the state the user sees. The focus is the contract between those stages: what must remain stable, what must be transformed, and where a platform-specific implementation takes over. That model lets readers reason about the request that actually reaches an API, rather than treating the editor's visible fields as the whole request.
A URL is only an address. Even a minimal useful request also has a method, headers, parameters, authentication, a body, and an expected response. In Hoppscotch, those values can be inherited from collections, filled from environments, changed by scripts, or stored as part of a larger workspace artifact. The system must preserve their meaning while converting them into forms that a browser relay, native relay, proxy, or Agent can carry. That is why the data model and the execution model need a boundary between them.
Keeping old request meaning intact
The first contract is the data contract. Hoppscotch's data package owns schemas and migrations for collections, REST requests, GraphQL requests, environments, responses, cookies, and related public structures. A collection is not accepted merely because it looks like JSON. Its version is detected, validated against a schema, and, when necessary, moved upward through registered migrations. The current source maps collections through version 12, REST requests through version 17, and GraphQL requests through version 10.
Those numbers are less important than the reason they exist. A request saved before a new field was introduced should not become an opaque object that the current editor can only partially understand. A migration can add a default script field, preserve an existing folder relationship, or translate an older representation into the current shape. Collection translation also distinguishes GraphQL-shaped requests from REST-shaped requests rather than forcing the two protocols into one indistinct object. The result is a compatibility layer between persistence, imports and exports, server JSON, the interactive client, and the CLI.
This does not mean every malformed or foreign file can be repaired. A migration is a defined path from one known version to another, not a promise to infer the author's intent from arbitrary data. The repository's version maps demonstrate compatibility intent within the supported models. They do not by themselves prove that every historical format or damaged artifact will survive unchanged.
The distinction matters when a user changes a request. The editor may show an environment variable placeholder, a collection-level header, or an inherited authentication mode. The stored object is a durable description of that setup. It is not necessarily the final wire representation. Treating persisted data as the wire request would confuse reusable intent with the values and decisions needed for one execution.
From intent to an effective request
Before the network boundary, Hoppscotch resolves the conditions under which this particular run will occur. The interactive runner captures the initial environment state, combines request, collection, temporary, selected-environment, and global variables according to precedence, and runs pre-request scripts. Those scripts can prepare values or update the request context. Authentication is then transformed into a typed representation, and the body is converted according to its content type.
This ordering is causal, not cosmetic. Suppose a URL contains an environment variable, an authorization token is stored in an environment, and a pre-request script refreshes that token. Sending the original editor object would send placeholders or stale credentials. The request must first become effective: a concrete endpoint, concrete headers and parameters, a body that matches its declared media type, and an authentication form that the selected transport understands.
Hoppscotch keeps this work separate from the transport adapter. Active headers and parameters become records suitable for the kernel request. Authentication becomes a typed union rather than a UI-specific collection of fields. JSON, XML, form data, multipart data, binary content, and text are represented as standardized content kinds. This separation prevents the UI's storage format from dictating how every platform must serialize the request.
It also creates a place for failure before any network call. Invalid variables, an unusable body, an unsupported authentication conversion, or a script error can stop the request while it is still being prepared. A clean boundary makes that different from a DNS error, a refused connection, an API response with an error status, or a failure while rendering the response. These are different observations, and confusing them makes debugging harder.
The adapter preserves intent while changing representation
The conversion from editor state to an effective request is not a single serialization step. It is a sequence of decisions about which values are active, which values are inherited, and which values belong only to this run. A collection-level header may be available to a child request until that request supplies its own value. An environment variable may appear in a URL, a header, a parameter, or a body. A script may update the request before execution, while a test script may update an environment only after a response exists. The effective request is the result of those relationships, not a copy of one object.
This is why the shared adapters do not ask each relay to understand the editor's internal schema. The REST request adapter filters the active metadata, converts the method to the transport vocabulary, and supplies a stable protocol version. The authentication adapter turns UI-specific modes into a typed form that a relay can inspect. The content adapter decides whether the body is text, structured JSON, XML, form data, multipart content, or binary data. Each conversion keeps the meaning that matters while discarding presentation details that do not belong on the wire.
The separation is especially useful when the same artifact is used by different clients. A collection can contain both REST and GraphQL requests, but their saved shapes and protocol rules are not identical. The data layer preserves those distinctions through separate versioned models. The execution layer can then adapt a GraphQL request to its HTTP representation without requiring the collection format to look like a generic REST request. The CLI follows a related pattern: it can use a REST-shaped execution pipeline for GraphQL-over-HTTP while retaining the raw query and variables long enough to apply templating and validate the eventual payload. Reuse comes from sharing stages and semantics, not from pretending that protocols have no differences.
The same principle governs authentication. The editor needs fields a person can understand—credentials, tokens, key placement, or a chosen grant. The transport needs a precise instruction about what to attach to headers or parameters and what the selected relay can support. Resolving that instruction before the relay runs makes the failure legible. A malformed or unsupported auth mode is a request-preparation problem; a rejected credential returned by the API is a server interaction problem. The two may look similar to a user who sees only “request failed,” but they require different remedies.
The body follows an equivalent path. A user can edit raw text, structured JSON, form entries, or a file. The application must preserve the declared media type and create a representation the chosen relay can transmit. A browser and a native relay may serialize the same logical body differently because their host APIs differ. By placing that conversion in a shared content boundary, Hoppscotch can keep the editor's behavior stable while allowing platform implementations to handle files, streams, or multipart data according to their own capabilities.
This layered conversion also makes the system testable. A request can be inspected after variable substitution but before network execution. A response can be examined before a post-request script changes environment state. A typed adapter can report that it could not create a valid content or auth representation without implying that the target server was contacted. The more carefully these stages are separated, the less likely it is that a failure in one stage will be misdiagnosed as a failure in another.
The kernel contract is the handoff point
Once the effective request is prepared, the common application converts it into a typed RelayRequest. The kernel contract carries the method, protocol version, URL, headers, parameters, content, authentication, security settings, proxy options, and advanced metadata. The application does not need to know whether the next implementation is Axios in a browser or a native relay using system-level capabilities. It asks for the contract's operation and receives a typed response or a typed error.
The kernel does not erase differences between implementations. It describes capabilities explicitly. A relay can advertise which methods, content forms, authentication modes, security controls, proxy protocols, and advanced options it supports. Its canHandle operation can reject a request as an unsupported feature before execution. The contract also represents cancellation, progress and state-change events, even though a particular implementation may provide some hooks more fully than another.
This is a useful form of abstraction because it standardizes the question, not necessarily the answer. The shared application can ask every path to execute a request with a common shape. A web implementation can reject client certificates. A native implementation can accept them. Both behavior patterns fit the same contract. Portability therefore means that the surrounding workflow survives; it does not mean that every environment has the same transport power.
The store and IO modules follow a related principle. Storage exposes namespaces, watches, temporary or permanent options, encryption and compression flags, and structured errors. IO exposes operations such as saving a file through a dialog, opening an external link, and listening for events. The application can depend on these concepts without embedding desktop APIs into every feature. The contract is versioned so that compatibility itself becomes explicit.
The response is part of the contract too
The return trip is as important as request preparation. A relay response contains a status, status text, protocol version, headers, a byte body with media type, timing, size metadata, and optional cookies or transport details. The common response adapter turns that into the UI's response model. It can render text, JSON, XML, images, or other bytes without asking the relay to decide how the interface should display them.
Keeping the body as bytes until the response layer matters. A response body that happens to look like JSON is not necessarily safe to parse as JSON, and a binary download must not be converted into a lossy string merely because the request originated in a web interface. Media type and byte representation preserve enough information for the UI and tests to make their own decisions. Timing and size metadata connect the result to performance inspection without changing the response's semantic body.
Headers have their own boundary cases. The response adapter normalizes them for the common model and has special handling for multiple Set-Cookie values that may cross a typed boundary as a newline-separated string. That workaround illustrates a broader point: a contract can reduce coupling without making serialization problems disappear. When information is compressed at a boundary, the receiving layer may need a careful, explicit restoration step.
The request stream also preserves distinct outcomes: loading, successful response, network failure, interceptor error, transformation failure, and cancellation. An API returning a 404 is not the same as failing to connect. A selected interceptor rejecting a client certificate is not the same as the server rejecting a valid request. These distinctions give users a more accurate model of where the workflow stopped.
The sequence has two kinds of ownership. The common application owns the meaning of the request, its preparation, and the user-facing interpretation of the result. The active interceptor and relay own the mechanics and limits of reaching the target. State write-back is selective: a test can affect environments or cookies only through the rules of the current platform and run. That prevents a successful request from becoming an excuse to persist every value it touched.
Shared semantics, deliberately different state
Secrets make the separation especially important. The request runner treats secret environment values as local-sensitive state and clears their values from server-bound write-back. Non-secret environment changes can be compared with the initial state and written to the appropriate environment when the run permits it. Cookie use depends on platform flags, and isolated runs can opt out of shared environments, cookies, and history altogether.
This is not merely a privacy feature added after the request pipeline. It follows from the contract model. A shared collection describes reusable request behavior; it should not automatically absorb one user's runtime token, cookie jar, or temporary test value. Local execution state and collaborative state have different owners and different lifecycles. The system is more predictable when the boundary is explicit, even if that means more decisions must be represented.
The same principle explains why a data migration, a request adapter, a kernel relay, and a response transformer all exist. Each protects a meaning while crossing a boundary. The data layer protects the meaning of a saved artifact across versions. The effective-request layer protects the meaning of user intent after variables and scripts are applied. The kernel protects a common transport vocabulary. The response layer protects the meaning of returned bytes and diagnostics. The state layer protects the distinction between reusable work and local runtime residue.
Hoppscotch's request is therefore best understood as a contract with stages, not as a single object passed unchanged from the editor to the network. That contract reduces duplication because the shared application does not need one complete request pipeline per platform. It also exposes where duplication and adaptation remain necessary, because a browser cannot inherit native capabilities merely by sharing a TypeScript type.
The next question follows directly: if the request contract is stable, what changes when different execution paths are asked to honor it? The answer lies in the capability boundaries of the browser, proxy, extension, native relay, and Agent—and in the conditions under which each one should be chosen.
References
- Interactive request runner
- Network boundary
- REST request adapter
- REST response adapter
- Auth adapter
- Content adapter
- Data package manifest
- Collection contract
- Collection v12 migration
- REST request contract
- GraphQL request contract
- Kernel entry
- Relay v1 contract
- Store v1 contract
- IO v1 contract
- Kernel README
- Platform feature flags
- Agent store
- Native store