Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/server/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import { isDevProxiedPath } from "@t3tools/shared/devProxy";
import { decodeOtlpTraceRecords } from "@t3tools/shared/observability";
import * as Data from "effect/Data";
import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
Expand Down Expand Up @@ -49,6 +50,9 @@ import * as ServerEnvironment from "./environment/ServerEnvironment.ts";
import { browserApiCorsAllowedHeaders, browserApiCorsAllowedMethods } from "./httpCors.ts";

const OTLP_TRACES_PROXY_PATH = "/api/observability/v1/traces";
// Generous for a typically-local collector; a stalled one must not park the
// proxy fiber forever. Expiry reuses the warning + 502 contract below.
const OTLP_TRACES_EXPORT_TIMEOUT = Duration.seconds(30);
const LOOPBACK_HOSTNAMES = new Set(["127.0.0.1", "::1", "localhost"]);
const DESKTOP_RENDERER_ORIGINS = ["t3code://app", "t3code-dev://app"];
const SVG_CONTENT_SECURITY_POLICY = "default-src 'none'; style-src 'unsafe-inline'; sandbox";
Expand Down Expand Up @@ -349,6 +353,7 @@ export const otlpTracesProxyRouteLayer = HttpRouter.add(
headers: otlpHeaders,
})
.pipe(
Effect.timeout(OTLP_TRACES_EXPORT_TIMEOUT),
Effect.flatMap(HttpClientResponse.filterStatusOk),
Effect.as(HttpServerResponse.empty({ status: 204 })),
Effect.tapError((cause) =>
Expand Down
27 changes: 27 additions & 0 deletions apps/server/src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5879,6 +5879,33 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
}).pipe(Effect.provide(NodeHttpServer.layerTest)),
);

it.effect("returns 502 when the OTLP collector stalls instead of hanging the export", () =>
Effect.gen(function* () {
const exportStarted = yield* Deferred.make<void>();
yield* buildAppUnderTest({
config: { otlpTracesUrl: "http://collector.test/v1/traces" },
layers: {
httpClient: HttpClient.make(() =>
Deferred.succeed(exportStarted, undefined).pipe(Effect.andThen(Effect.never)),
),
},
});
const cookie = yield* getAuthenticatedSessionCookieHeader();

const posting = yield* HttpClient.post("/api/observability/v1/traces", {
headers: { cookie, "content-type": "application/json" },
body: yield* HttpBody.json({ resourceSpans: [] }),
}).pipe(Effect.forkChild({ startImmediately: true }));
// Wait until the collector forward is in flight; advancing the clock
// earlier would trip unrelated timeouts and pass vacuously.
yield* Deferred.await(exportStarted);
yield* TestClock.adjust(Duration.seconds(31));
const response = yield* Fiber.join(posting);

assert.equal(response.status, 502);
}).pipe(Effect.provide(NodeHttpServer.layerTest)),
);

it.effect("routes websocket rpc server.upsertKeybinding", () =>
Effect.gen(function* () {
const rule: KeybindingRule = {
Expand Down
Loading