Skip to content

ADFA-5176: Serve documentation to the app's WebViews in-process - #1726

Open
davidschachterADFA wants to merge 12 commits into
task/ADFA-5241-charsetfrom
task/ADFA-5176-in-process-docs
Open

ADFA-5176: Serve documentation to the app's WebViews in-process#1726
davidschachterADFA wants to merge 12 commits into
task/ADFA-5241-charsetfrom
task/ADFA-5176-in-process-docs

Conversation

@davidschachterADFA

@davidschachterADFA davidschachterADFA commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #1725 — this PR's base is task/ADFA-5241-charset, so its diff is just this ticket's work. GitHub retargets it to stage when #1725 merges.

One pipeline for reading documentation.db, in common, with two transports over it.

DocumentationContentSource owns row lookup, chunked-row reassembly, the dictionary-aware Brotli decode and the sdcard debug-database swap, under a read/write lock so readers don't have the handle closed under them. It also renders the Pebble template rows, so both transports serve finished pages and neither carries the template engine.

DocumentationRequestInterceptor answers the app's own WebViews through WebViewClient.shouldInterceptRequest, so a page's assets cost a database read instead of a TCP connection each. It matches the same http://localhost:6174/... URL space, so strings.xml, ToolTipManager's link builder and the DocumentationExtension contract need no changes; anything it declines — a /pr/ endpoint, an unknown path, a failed read — falls through to WebServer.

WebServer keeps port 6174 for WebViews not wired to the interceptor and for the /pr/ endpoints, but now reads through the shared source: no database handle, no Pebble engine, no gson, no decode path of its own. What's left is HTTP.

This is a port, not the original branch

The original was stacked on ADFA-5172 and ADFA-5175. Those are an abandoned investigation and a declined design, so this branch carries neither: the accept loop is stage's single-threaded one, and the stallThresholdMs/maxWorkerThreads config fields, the stall instrumentation and the worker pool are all gone.

The two rules the original duplicated are now shared rather than repeated:

  • The dictionary is gated on the version the database declares (DatabaseVersionResolver.resolveMajorVersion), as WebServer already does, instead of on whether a CompressionDictionary table happens to exist.
  • The charset comes from ContentTypeHeaders (ADFA-5241). The interceptor previously declared utf-8 for text/ only, so an SVG served in-process declared no encoding while the same row over the socket declared one.

Verified on hardware

SM-N986U, both transports, same page and same build. In-process (default):

D DocumentationRequestInterceptor: Served 'a/android/R.id.html' in-process, 115518 bytes.
I HelpActivity: Loaded 'http://localhost:6174/a/android/R.id.html' in 142 ms;
                2 requests, 231036 bytes served in-process.
D DocumentationRequestInterceptor: Served 'favicon.ico' in-process, 0 bytes.

grep -c 'Request is GET /a/android/R.id.html' against the server: 0. The socket never saw it. 115,518 bytes matches that row's plaintext size exactly, and the page renders correctly in the app's WebView.

Fallback, with /sdcard/Download/CodeOnTheGo.nointercept present:

D WebServer:     Request is GET /a/android/R.id.html HTTP/1.1
I HelpActivity:  Loaded '...' in 99 ms; in-process serving is off (Download/CodeOnTheGo.nointercept exists).

Driving HelpActivity from adb needs android:exported="true", which I added locally to measure and reverted before committing — the manifest in this PR is unchanged.

On the numbers: 142 ms in-process against 99 ms over the socket. These are single samples on a page with one asset, so they show both paths working, not a speedup. The connection-per-asset cost this ticket targets only shows up on pages with many assets, and I have not measured that fairly — worth doing before anyone claims a performance win.

Tests

356 across app and common, 0 failures: 14 for the content source, 9 for the interceptor, 9 for ContentTypeHeaders, 7 for the dictionary decode, 5 for the server. The two WebServerTest cases asserting the dictionary loads once per database now declare a version — without that the new gate would have left them passing while testing nothing.

🤖 Generated with Claude Code

One pipeline for reading documentation.db, in common, with two transports over
it. DocumentationContentSource owns row lookup, chunked-row reassembly, the
dictionary-aware Brotli decode and the sdcard debug-database swap, under a
read/write lock so readers do not have the handle closed under them. It also
renders the Pebble template rows, so both transports serve finished pages and
neither carries the template engine.

DocumentationRequestInterceptor answers the app's own WebViews through
WebViewClient.shouldInterceptRequest, so a page's assets cost a database read
instead of a TCP connection each. It matches the same
http://localhost:6174/... URL space, so strings.xml, ToolTipManager's link
builder and the DocumentationExtension contract need no changes, and anything
it declines -- a /pr/ endpoint, an unknown path, a failed read -- falls through
to WebServer. The CodeOnTheGo.nointercept sentinel forces everything back onto
the socket.

WebServer keeps serving port 6174 for WebViews that are not wired to the
interceptor and for the /pr/ developer endpoints, but it now reads through the
shared source: no database handle, no Pebble engine, no gson, no decode path of
its own. What is left is HTTP.

This is a port rather than the original branch. ADFA-5172 was an investigation
whose instrumentation is abandoned, and ADFA-5175's worker pool is declined, so
the accept loop here is stage's single-threaded one and the config fields those
tickets added are gone. The two duplicated rules the original carried are now
shared instead:

  * The dictionary is gated on the version the database declares
    (DatabaseVersionResolver.resolveMajorVersion), as WebServer already does,
    rather than on whether a CompressionDictionary table happens to exist.
  * The charset comes from ContentTypeHeaders (ADFA-5241), so a row does not
    describe itself differently depending on which transport served it. The
    interceptor previously said utf-8 for text/ only, which left an SVG served
    in-process declaring no encoding while the same row over the socket
    declared one.

356 tests across app and common pass, including 14 for the content source and 9
for the interceptor. The two WebServer tests that assert the dictionary loads
once per database now declare a version, without which the gate would leave
them passing while testing nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

davidschachterADFA and others added 3 commits August 21, 2026 21:03
…docs

# Conflicts:
#	app/src/main/java/com/itsaky/androidide/localWebServer/WebServer.kt
#	app/src/test/java/com/itsaky/androidide/localWebServer/WebServerTest.kt
Ten findings from the review on PR #1726. The three that could break something
in production:

* switchToDatabase bumped generation and closed the old handle but never
  cleared templateCache, so Pebble templates compiled from a closed database
  kept rendering. The old WebServer.switchToDatabase cleared it and the field
  comment still promised it. An edited template row in a swapped-in database
  would have rendered the previous database's markup for the rest of the
  session.

* The brotli warm-up did not move with the decode. WebServer.decompressBrotli
  wrapped Brotli4jLoader.ensureAvailability() so a missing native library cost
  one failed read instead of the process; when the decode moved into
  DocumentationContentSource the guard stayed behind, and an
  UnsatisfiedLinkError is an Error, so it escapes every catch between there and
  the accept loop. Restored on the decode path, where the decode now is.

* The interceptor's `shared` initializer called ensureAvailability() eagerly.
  That runs during Activity and Fragment construction, so a missing library was
  a hard crash on opening Help rather than a failed page. Removed: the source
  warms lazily and converts the Error, which covers both transports.

mimeAndCharset now delegates to ContentTypeHeaders.typeAndCharset instead of
re-parsing with substringAfter("charset="). The duplicate parse disagreed with
the socket transport on quoted parameters and on case -- for `; Charset=UTF-8`
it missed the parameter and the response went out with no encoding at all,
which is the ADFA-5241 failure this class's KDoc claims to prevent. sendContent
also builds its header before the status line, so a throw cannot append a
second status line to a response that already claimed 200.

Six unused imports left in WebServer.kt -- four of them the ADFA-5175 worker
pool's -- would have failed spotlessCheck in CI under the file-level ratchet.

HelpActivity loaded every page twice: once in onCreate and again through
updateUIFromIntent. This PR's own device log proves it -- "2 requests, 231036
bytes" for one 115,518-byte page -- so every open paid two full reads, and the
142 ms figure quoted against the socket path's 99 ms was timing two loads
against one. The instrumentation also used wall-clock time (an NTP correction
mid-load reports nonsense) and presented process-cumulative counters as the
page's own; it now uses elapsedRealtime and says "totals so far".

The three ADFA-5220 version-gate tests deleted when WebServerTest was replaced
are restored in DocumentationContentSourceTest, where dictionaryBytes now
lives: below 2, no version table, and above 2, with the dictionary cursors
stubbed as available in every case so they test the gate rather than a missing
table.

failedDebugSwapTimestamp is @volatile: the check reads it outside the write
lock, so without it a second thread misses the first's failure marker and
re-attempts openDatabase on a broken file while holding the lock -- and a
64-bit read is not atomic on armeabi-v7a.

Three WebSettings property reads with no assignment were deleted rather than
turned into `= true`. They are no-ops today; assigning them would silently
enable three security-relevant settings, universal file access among them, as a
side effect of a reindentation. If the fragment's file:///android_asset
handling needs file access, that is its own change with its own reasoning.

Also: comments describing a worker pool, awaitTermination and a templateCache
this class no longer has, and ARCHITECTURE.md's raw-SQLite exception, which
still named WebServer as the holder of the database handle.

363 tests across app and common pass; spotlessCheck is clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The three that needed a decision rather than a patch.

**Cache staleness was judged before the swap it depends on.** serveRequest
called discardCachesIfDatabaseChanged() first, but the source performs the
debug-database swap inside lookup()/withDatabase(). On the request that swaps,
the generation read was the pre-swap one, so bookshelfTemplateId still pointed
at the previous database's template row -- rendering the old bookshelf, or 500ing
when that id does not exist in the new database. The source now exposes
refreshDatabase(), which applies a pending swap without reading, and the discard
runs after it. Idempotent and throttled by the debug check interval, so the cost
is one extra timestamp comparison.

**Priming the dictionary could fail an unrelated lookup.** readContent primes it
before reading the row, and dictionaryBytes deliberately lets unexpected
failures propagate so a transient error is not cached as "no dictionary". On the
lookup path that meant a locked database during one dictionary query failed
*every* request, including rows with compression = 'none' that need no
dictionary at all. The priming call is now best-effort: it logs, leaves the
staleness flag set so the next read retries, and a brotli row that genuinely
cannot resolve its dictionary still fails loudly from inside decompressBrotli.

**The two transports disagreed about what a path is.** The interceptor matches
WebResourceRequest.url.path, which is percent-decoded; WebServer matched the raw
target. For any path the WebView encodes -- a space, a literal % -- the
interceptor found the row and the server 404ed it, so setting the nointercept
sentinel changed *which pages work*, defeating its purpose of comparing the two
transports on equal terms. WebServer now decodes, with two details worth
keeping: "+" is protected first, because URLDecoder alone turns it into a space
and would break a stored path containing a literal plus (c++.html is a real
shape here), and a malformed escape logs and falls back to the verbatim path
rather than failing the request, so it 404s naturally.

Three tests on the decoding -- encoded space, literal plus, malformed escape --
assert the path the server actually queries with, not just that a request
succeeds. 366 tests across app and common pass; spotlessCheck is clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@davidschachterADFA

Copy link
Copy Markdown
Collaborator Author

All fifteen review findings are now addressed across 7d47b098 and fd4c8fa0. Summary for anyone reading the PR rather than the commits.

Would have broken in production

  • switchToDatabase never cleared templateCache — Pebble templates compiled from a closed database kept rendering. The old WebServer.switchToDatabase cleared it, and the field comment still promised it did.
  • The brotli warm-up did not move with the decode. WebServer.decompressBrotli wrapped Brotli4jLoader.ensureAvailability() so a missing native library cost one failed read instead of the process; the guard stayed behind when the decode moved to common, and UnsatisfiedLinkError is an Error, so it escapes every catch (e: Exception) between there and the accept loop.
  • The interceptor warmed brotli eagerly in shared's initializer, which runs during Activity/Fragment construction — turning a failed page into a hard crash on opening Help. Removed; the source warms lazily and converts the Error, covering both transports.
  • Six unused imports in WebServer.kt, four of them the abandoned worker pool's, would have failed spotlessCheck in CI under the file-level ratchet.

Wrong answers rather than crashes

  • Cache staleness was judged before the swap it depends on. The source swaps inside lookup()/withDatabase(), so checking generation first read the pre-swap value: on the swapping request, bookshelfTemplateId still pointed at the previous database's template row. refreshDatabase() now applies a pending swap without reading, and the discard runs after it.
  • Priming the dictionary could fail an unrelated lookup — a locked database during one dictionary query failed every request, including compression = 'none' rows needing no dictionary. Priming is best-effort now; a brotli row that genuinely cannot resolve its dictionary still fails loudly from inside the decode.
  • The two transports disagreed about what a path is. The interceptor matches url.path (decoded), WebServer matched the raw target, so an encoded space meant the interceptor found the row and the server 404ed it — the nointercept sentinel changed which pages work. WebServer decodes now, protecting + first (URLDecoder alone turns it into a space, and c++.html is a real shape here) and falling back to the verbatim path on a malformed escape.
  • mimeAndCharset re-parsed the charset with substringAfter("charset="), disagreeing with the socket transport on quoted parameters and on case — for ; Charset=UTF-8 it missed the parameter entirely and the response went out with no encoding, the exact ADFA-5241 failure its KDoc claims to prevent. It delegates to ContentTypeHeaders.typeAndCharset now.
  • sendContent built its header after the status line was already flushed, so a throw appended a second status line to a response that had claimed 200.
  • failedDebugSwapTimestamp is @Volatile — the check reads it outside the write lock, and a 64-bit read is not atomic on armeabi-v7a.

Caught in my own evidence

HelpActivity loaded every page twice, once in onCreate and again through updateUIFromIntent. The proof was in this PR's own device log: "2 requests, 231036 bytes" for one 115,518-byte page. So the 142 ms in-process figure was timing two loads against the socket path's one, and the comparison I flagged as "not a speedup" was worse than that. The instrumentation also used wall-clock time and presented process-cumulative counters as the page's own; it uses elapsedRealtime now and says "totals so far".

The three ADFA-5220 version-gate tests deleted when WebServerTest was replaced are restored in DocumentationContentSourceTest, where dictionaryBytes now lives.

Declined, with reasons

Three WebSettings property reads with no assignment were deleted rather than turned into = true. They are no-ops today; assigning them would silently enable three security-relevant settings — universal file access among them — as a side effect of a reindentation. If the fragment's file:///android_asset handling needs file access, that deserves its own change and its own reasoning.

Test-helper duplication and Truth-vs-JUnit assertions are left as they are: both are worth doing, neither is worth expanding this PR's diff for, and the JUnit style matches the file this test was ported from.

Also fixed: comments describing a worker pool, awaitTermination and a templateCache this class no longer has, and ARCHITECTURE.md's raw-SQLite exception, which still named WebServer as the holder of the database handle.

366 tests across app and common pass; spotlessCheck is clean.

@davidschachterADFA

Copy link
Copy Markdown
Collaborator Author

On-device re-verify (Galaxy Note 20 Ultra, arm64, v8 debug)

The numbers previously in this PR predated 7d47b0986 and fd4c8fa0c, which changed exactly what they measured — brotli priming moved into DocumentationContentSource, the interceptor's eager ensureAvailability() came out, and HelpActivity stopped loading every page twice. Re-measured on hardware from a fresh install; both transports from the one build via the CodeOnTheGo.nointercept sentinel.

Page Transport Socket connections In-process requests Load
i/index.html in-process 0 16 (144,651 bytes) 137 ms
i/index.html socket 39 0 269 ms
k/html/basic-syntax.html (templated) in-process 0 4 55 ms
k/html/basic-syntax.html (templated) socket 6 0 56 ms

Renders are pixel-identical across transportscompare -metric AE returns 0 differing pixels for both pages (status bar cropped, so the clock can't count as a difference). The templated page matters most here: Pebble now renders it in common, so this is the first hardware evidence that the in-process path produces the same page as the socket path rather than merely a page.

The three review fixes, checked individually

  • Double load is gone. i/index.html was served exactly once (grep -c "Served 'i/index.html'" = 1). The tripled .js fetches in the log are the frameset's three frames each pulling the same script — normal browser behaviour, not the regression.
  • Brotli priming after the move: no UnsatisfiedLinkError, no dictionary-decode failures, no W/E from any documentation class across every run. 144 KB of brotli content decoded and rendered.
  • Template cache cleared on swap: after touching the debug database, Swapped to the debug database '/storage/emulated/0/Download/documentation.db' followed by a correct render, and the same 144,651-byte total as the pre-swap run.

Boundaries hold

  • /pr/db with interception on: 1 socket connection, 0 in-process. The developer endpoints stay on the socket server, as contentFor intends with its path.startsWith("pr/") decline.
  • The sentinel reports itself rather than going quiet: in-process totals so far: in-process serving is off (Download/CodeOnTheGo.nointercept exists).

Accessibility

Verified at font scale 1.3 (the device's own setting) and 2.0: text reflows, nothing clipped or overrun, both system bars intact, content still scrollable. Scale restored afterwards.

What I am not claiming

The 137 ms vs 269 ms gap is one page, one device, one run each, warm-vs-cold not controlled — directionally consistent with removing a loopback socket per request, but not a benchmark. The many-asset comparison this PR's description mentions still has not been measured fairly.

davidschachterADFA and others added 2 commits August 24, 2026 11:44
It was a data class whose equals/hashCode were overridden back to identity,
because generated equality over a ByteArray compares identity anyway and
comparing multi-megabyte content is not what any caller wants. What that left
behind was copy(), which returned an object unequal to its source.

Nothing here needs value semantics, so the class no longer offers them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…uction

HelpActivity and IDETooltipWebViewFragment forced
DocumentationRequestInterceptor.shared from a property initializer, so
the whole lazy ran during construction, before onCreate, on the main
thread. Two consequences.

Environment.DOC_DB is a plain static File with no initializer, assigned
only by Environment.init(). DeviceProtectedApplicationLoader wraps that
call in runCatching and the credential-protected loader returns before
it when storage is not ready, so null is a state the app can really be
in -- and the non-null parameter turned it into an NPE that killed the
activity before it existed. shared is nullable now and declines instead,
which puts the request back on the local web server: the same thing a
null from intercept() already means everywhere else.

The lazy also stats external storage for the nointercept sentinel. On
the main thread that is a disk read under a StrictMode policy built with
detectAll(), and on a contended FUSE mount it stalls the frame that
opens the screen. Touched from shouldInterceptRequest instead, on a
WebView thread, the way FAQActivity already did it.

Not covered here: intercept() still has no throw guard, so an Error
(an OOM decoding a large row) escapes onto a Chromium thread rather
than falling through to the server the way the class documents. That is
a separate finding from the same review.

Found in review of PR #1726.
@davidschachterADFA

Copy link
Copy Markdown
Collaborator Author

Pushed 8da9283 for the Environment.DOC_DB finding.

HelpActivity and IDETooltipWebViewFragment forced DocumentationRequestInterceptor.shared from a property initializer, so the whole lazy ran during construction, before onCreate, on the main thread. Two consequences:

  • Environment.DOC_DB is a plain public static File with no initializer, assigned only by Environment.init(). DeviceProtectedApplicationLoader wraps that call in runCatching, and the credential-protected loader returns before it when storage isn't ready — so null is a state the app can genuinely be in, and the non-null parameter turned it into an NPE that killed the activity before it existed. shared is nullable now and declines instead, which puts the request back on the local web server. That's the same thing a null from intercept() already means everywhere else, so the contract is unchanged.
  • The lazy also stats external storage for the nointercept sentinel. On the main thread that's a disk read under a detectAll() StrictMode policy, and on a contended FUSE mount it stalls the frame that opens the screen. Both call sites use by lazy now, so it happens on a WebView thread — the way FAQActivity already did it.

Left alone deliberately, from the same review: intercept() still has no throw guard, so an Error — an OOM decoding a large row — escapes onto a Chromium thread instead of falling through to the server the class documents. Worth its own change.

267 app + 99 common tests pass.

One formatting note in case anyone hits it: ktlint and the 140-column limit fight over that shouldInterceptRequest override. As an expression body it gets re-joined onto one 141-column line and spotlessApply then can't fix what it just made, so it's a block body now.

@itsaky-adfa itsaky-adfa left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness pass over the diff. Five findings, one of them a behavioural regression against stage's WebServer.

Comment thread app/src/main/java/com/itsaky/androidide/localWebServer/WebServer.kt Outdated
Comment thread app/src/main/java/com/itsaky/androidide/localWebServer/WebServer.kt
Comment thread common/src/main/java/com/itsaky/androidide/activities/editor/HelpActivity.kt Outdated
Comment thread app/src/main/java/com/itsaky/androidide/localWebServer/WebServer.kt Outdated
claude added 2 commits August 26, 2026 15:48
- switchToDatabase clears templateCache with the swap, restoring what
  stage's WebServer.switchToDatabase did; the comments saying templates
  are dropped on swap are true again.
- The cs0 clear-cache sentinel also clears the shared interceptor's
  source, not just the server's own. The interceptor's shared property
  is backed by an explicit Lazy so an interceptor that was never built
  is not created just to empty its cache.
- realHandleBsEndpoint answers an empty bookshelf join with a 500:
  group_concat over an empty subquery yields one row whose value is
  NULL, which isCursorOneRow passes and the null then fell out of
  withDatabase as a zero-byte closed connection.
- HelpActivity.onPageFinished no longer force-initializes the shared
  interceptor on the main thread: the lazy is explicit, and the load
  summary reads it only when shouldInterceptRequest already did.

Regression tests: a swap re-renders templates from the new database
(common), and /pr/bs over an empty join sends HTTP 500 (app).
- WebServer.serveRequest's caller talked about a read lock that no
  longer exists there; the swap now happens inside the content source.
- The note explaining sendRawGetRequestAndAwaitClose sat above an
  unrelated test; move it to the helper it documents.
@claude
claude Bot requested a review from itsaky-adfa August 26, 2026 15:51
claude and others added 3 commits August 26, 2026 15:57
…k/ADFA-5176-in-process-docs

# Conflicts:
#	ARCHITECTURE.md
Making `shared` nullable last round fixed the NPE and introduced a
quieter bug: `lazy` memoizes whatever the initializer returned,
including null. Environment.init() runs inside the loader coroutine --
DeviceProtectedApplicationLoader wraps it in runCatching, and the
credential-protected loader returns early when storage is not ready --
so a WebView that asks during direct boot saw DOC_DB unset, and that
answer was then cached for the life of the process. In-process
documentation stayed off afterwards, and since WebServer is started only
by MainActivity and stopped in its onDestroy, opening Help from the
editor later had nothing to fall back to either.

Only a successful construction is cached now; a null is retried on the
next request. clearSharedTemplateCache reads the field directly, so
asking whether the interceptor exists still cannot create it.

intercept() also catches Throwable. The class documents that anything it
returns null for falls through to the web server, and that only held for
values: decoding the largest bundled row (8.8 MB over nine chunks) can
raise OutOfMemoryError, a pathological template a StackOverflowError,
and lookup()'s catch (e: Exception) sees neither. This runs on a Chromium
thread, where an escaping Error takes the process down -- the socket
transport confined the same failure to one 500. The file's own
ensureBrotliAvailable KDoc describes exactly this hazard for the other
transport.

102 common tests pass, :app compiles.

Found in review of PR #1726.
@davidschachterADFA

Copy link
Copy Markdown
Collaborator Author

@itsaky-adfa — all five findings are closed. The verdict is pinned to 8da9283; head is now 8c29578.

# Finding Closed by Where to look
1 switchToDatabase never clears templateCache, so a debug swap keeps rendering the old database's templates 3336ad643 templateCache.clear() now sits between compressionDictionaryStale = true and generation++ — your suggestion applied verbatim
2 cs0 sentinel clears a cache no user-visible page reads 3336ad643 WebServer.kt:566-571 clears both: contentSource.clearTemplateCache() and DocumentationRequestInterceptor.clearSharedTemplateCache()
3 ?: return false swallows a genuine NULL blob; /pr/bs answers with zero bytes 3336ad643 Explicit if (json == null) branch that logs and sends a 500, with the group_concat-over-empty-join reason in the comment
4 servedSummary() can be the first touch of the lazy on the main thread 3336ad643 Guarded by documentationLazy.isInitialized(); falls back to "interceptor not yet used" rather than building the interceptor in onPageFinished
5 Stale comment at the serveRequest call site (and the drifted one in WebServerTest) 28d7608b1 Both rewritten to describe the code that is actually there

On #3 — worth recording that your diagnosis of why was the useful part. The platform-type inference (T inferring as ByteArray? because one branch is literally null, so no null check is generated) is what made it invisible, and it is the reason the same shape on stage NPE'd into a 500 while this version wrote nothing at all.

One commit since you looked that is not on your list: 8c29578ca stops caching a missing DOC_DB refusal forever, so a database that appears later is picked up instead of the process staying broken until restart.

Ready for another look.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants