Skip to main content

HttpBundleObservable

HttpBundleObservable is the object returned by client.bundle() and client.transaction(). It groups multiple HttpRequestObservable instances into a single POST /$bundle request with Content-Type: multipart/mixed, distributes each parsed sub-response back to its originating request, and exposes the full response array via getResponses().

Package: @opra/client
Extends: Observable<HttpResponse[]>


Overview

const r1 = client.get<Customer>('customers@1');
const r2 = client.get<Order[]>('orders');

const [c1, c2] = await client.bundle([r1, r2]).getResponses();
// Each HttpRequestObservable also resolves individually:
const customer = await r1.getBody();
const orders = await r2.getBody();

When requests are bound to a bundle they do not fire their own network calls — the bundle's single multipart/mixed POST drives them all. The individual getBody() / getResponse() calls block until the bundle completes and then resolve from a cached ReplaySubject, so you can await them before or after calling getResponses().


Methods

param(name, value?)

Sets a URL query parameter on the /$bundle request. Chainable.

param(name: string, value: any): this
param(params: Record<string, any>): this

Used internally by client.transaction() to append ?transaction=true. You can call it directly for any other query parameter the server supports.

client.bundle([r1, r2]).param('timeout', 5000).getResponses();

getResponses()

Sends the bundle request and resolves with the array of sub-responses in the same order as the input requests.

getResponses(): Promise<HttpResponse[]>

Each HttpResponse has the status, headers, and body of the corresponding sub-request. If a sub-request returned an error status, its slot in the array carries that error response — getResponses() itself does not throw for per-part errors.

const [res1, res2] = await client.bundle([r1, r2]).getResponses();
if (!res1.ok) console.error('r1 failed:', res1.status);

How the wire protocol works

HttpBundleObservable serializes each request using serializeHttpRequest() to produce raw HTTP/1.1 bytes. These become the parts of a multipart/mixed body sent as a single POST /$bundle. Each part carries an X-Request-Id header. The server echoes the X-Request-Id in the response part so the client can match sub-responses back to their originating requests.

POST /api/$bundle HTTP/1.1
Content-Type: multipart/mixed; boundary=---b

-----b
Content-Type: application/http
X-Request-Id: <uuid-1>

GET /api/customers@1 HTTP/1.1

-----b
Content-Type: application/http
X-Request-Id: <uuid-2>

GET /api/orders HTTP/1.1
-----b--

Transactions

Use client.transaction() instead of client.bundle() to wrap all sub-requests in a single database transaction:

const r1 = client.post('orders', { productId: 'p1', qty: 2 });
const r2 = client.patch('inventory/p1', { reserve: 2 });

await client.transaction([r1, r2]).getResponses();
// If either sub-request fails, the server rolls back both.

transaction() is equivalent to:

client.bundle([r1, r2]).param('transaction', true)

Subscribing as an Observable

Because HttpBundleObservable extends Observable<HttpResponse[]>, you can also use it with RxJS:

import { switchMap } from 'rxjs';

trigger$.pipe(
switchMap(() => client.bundle([r1, r2]))
).subscribe(responses => console.log(responses));

See also