Skip to content
Merged
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
2 changes: 1 addition & 1 deletion apps/docs/src/content/inspectors/ssr-http.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ The devtools don't write SSR mocks to TransferState, so the browser requests the

Client rules apply right away. SSR rules apply from the next page load, so the panel asks for a reload only when a rule applies on SSR. The page also keeps client rules in `sessionStorage`, so they apply on reload before the overlay connects. Rules live in the memory of the server process. They survive a Vite restart in the same process, such as after a config edit, and the **SSR & HTTP** tab keeps showing them. A new process starts with none.

If the `http` inspector or `actions.http` is off, the server clears its rules when it starts. The overlay removes the stored client rules when it connects with the `http` inspector off, so requests made before it connects on that load can still fail.
If the `http` inspector or `actions.http` is off, the server clears its rules when it starts. SSR rules apply only while the devtools server runs, so they stop when it closes, for example after a config edit that removes the Vite plugin. The overlay removes the stored client rules when it connects with the `http` inspector off, so requests made before it connects on that load can still fail.

### Timeline and rule caps

Expand Down
62 changes: 60 additions & 2 deletions packages/devtools/src/__tests__/vite-restart.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import '@angular/compiler';
import { Injector, PLATFORM_ID, runInInjectionContext } from '@angular/core';
import { HttpRequest, HttpResponse, type HttpEvent } from '@angular/common/http';
import { createHostContext } from 'devframe/node';
import { beforeEach, describe, expect, it } from 'vitest';
import { of, type Observable } from 'rxjs';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import pangular from '../devframe.ts';
import { clearCalls, recordCall, type AnalogCall } from '../analog-server-log.ts';
import { httpRegistry } from '../http-rules.ts';
import { pangularHttpInterceptor } from '../http.ts';
import { httpRegistry, type HttpRule } from '../http-rules.ts';
import type { AnalogState } from '../rpc/analog-tools.ts';
import { releaseServerState } from '../vite.ts';

Expand Down Expand Up @@ -59,3 +64,56 @@ describe('releaseServerState', () => {
expect((await current.analog()).calls).toHaveLength(0);
});
});

describe('server fault rules across a Vite restart', () => {
const g = globalThis as { ngDevMode?: unknown };
const saved = g.ngDevMode;
const rule: HttpRule = {
id: 'r1',
pattern: '/api',
enabled: true,
target: 'server',
status: 503,
};
const injector = Injector.create({ providers: [{ provide: PLATFORM_ID, useValue: 'server' }] });
const ssrStatus = () =>
new Promise<number>((resolve) => {
const result = runInInjectionContext(injector, () =>
pangularHttpInterceptor(new HttpRequest('GET', 'http://localhost/api/items'), () =>
of(new HttpResponse({ status: 200 })),
),
) as Observable<HttpEvent<unknown>>;
result.subscribe({
next: (event) => {
if (event instanceof HttpResponse) resolve(event.status);
},
error: (error: { status: number }) => resolve(error.status),
});
});

beforeEach(() => {
g.ngDevMode = true;
});

afterEach(() => {
g.ngDevMode = saved;
httpRegistry().dispose?.();
delete httpRegistry().rules;
});

it('keeps applying the rules when a new server takes over in the same process', async () => {
const old = await boot();
httpRegistry().rules = [rule];
await boot();
releaseServerState(old.ctx);
expect(await ssrStatus()).toBe(503);
});

it('stops applying the rules once the server that owns them closes', async () => {
const current = await boot();
httpRegistry().rules = [rule];
expect(await ssrStatus()).toBe(503);
releaseServerState(current.ctx);
expect(await ssrStatus()).toBe(200);
});
});
3 changes: 2 additions & 1 deletion packages/devtools/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,8 @@ export const pangularHttpInterceptor: HttpInterceptorFn = (req, next) => {
pageUrl = location.pathname + location.search;
}
const url = req.urlWithParams;
const rules = side === 'client' ? clientRules() : httpRegistry().rules;
const registry = httpRegistry();
const rules = side === 'client' ? clientRules() : registry.record && registry.rules;
const rule = matchRule(url, req.method, rules, side);
const started = Date.now();
const delay = Math.min(Math.max(rule?.delayMs ?? 0, 0), MAX_DELAY_MS);
Expand Down
Loading