Skip to content

Serve the translated document over loopback instead of files - #125

Merged
andiwand merged 2 commits into
mainfrom
core-http-server
Jul 26, 2026
Merged

Serve the translated document over loopback instead of files#125
andiwand merged 2 commits into
mainfrom
core-http-server

Conversation

@andiwand

@andiwand andiwand commented Jul 26, 2026

Copy link
Copy Markdown
Member

🤖 Generated with Claude Code

Why

odrcore rendered every page of a document before any of them was shown, on whichever thread called translate — the main thread whenever a password is entered or editing is toggled. odr::HttpServer, which OpenDocument.droid already uses, turns that around:

  • odr::html::translate returns an HtmlService that has rendered nothing
  • the service is connected to a server bound to 127.0.0.1:29665
  • the web view is pointed at http://127.0.0.1:29665/file/<prefix>/<page>.html
  • odrcore renders that page, on one of the server's threads, when the request arrives

So a page nobody opens is never rendered, the work that does happen is off the calling thread, and turning a page is a navigation rather than another translation — Document.page no longer re-parses the file to show a page whose address it already had.

The <prefix> changes on every translation: the web view caches by URL, and a document re-translated after a password or an edit has to land on an address it has not seen.

Permissions — the question that was asked

Nothing here needs a capability or prompts the user:

  • a listening socket on loopback takes no entitlement
  • the local network permission from iOS 14 covers the local subnet and multicast, not 127.0.0.1
  • App Transport Security does block plain HTTP, so both Info.plists gain NSAllowsLocalNetworking — the narrow exception for local addresses, which unlike NSAllowsArbitraryLoads needs no justification in App Store review

testTheWebViewLoadsAServedPage covers exactly this: it drives a real WKWebView at a served page, which is the only place a missing ATS exception would show up (URLSession is happy either way).

Trade-off worth knowing

A document that only falls over halfway through rendering now fails while the page is being served rather than inside translate, so that failure is no longer caught there. DocumentViewController therefore acts as the web view's navigation delegate and routes a 4xx/5xx back into documentLoadingError, so the user still gets the error page or the raw file rather than the server's plain text "Internal Server Error".

Escape hatch

kCoreWrapperServesOverHttp in CoreWrapper.mm switches back to writing the pages out as files, which is also what happens by itself when the port cannot be bound. It exists to keep that path working while the migration finishes, not as a runtime option.

Build changes

  • conan/conanfile.py: odrcore/*:with_http_server=True
  • conan/profiles/ios-base: -framework CoreFoundation -framework CFNetwork for odrcore's executables. cpp-httplib resolves host names through CFHost on Apple platforms and odrcore's CMake links neither framework, so its cli/server tool fails to link. (ODR_CLI=OFF would be tidier but odrcore 5.5.0 installs the cli targets whether or not it built them — worth fixing upstream.)
  • the app target links CoreFoundation and CFNetwork for the same reason
  • HtmlConfig.relative_resource_paths is off in server mode; odrcore rejects the combination, since there is no output directory to be relative to

conan/setup-all.sh has to be re-run — odrcore is rebuilt with the http server.

Verified

xcodebuild test on the iPhone 17 simulator (26 tests, all pass) and a device build of the Lite/Release slice.

odrcore rendered every page of a document before any of them was shown,
on whichever thread called translate - the main one whenever a password
was entered or editing was toggled. `odr::HttpServer`, which is what
OpenDocument.droid already uses, turns that around: `translate` hands
back a service that has not rendered anything, the service is connected
to a server bound to 127.0.0.1:29665, and odrcore renders a page on one
of the server's threads when the web view asks for it.

So a page nobody opens is never rendered, the work that is done happens
off the calling thread, and turning to another page is a navigation
rather than another translation - `Document.page` no longer re-parses
the file to show a page whose address it already had.

The prefix in the URL changes on every translation. The web view caches
by URL, and a document re-translated after a password or an edit has to
come back at an address it has not seen before.

Falling over now happens while the page is being served rather than in
`translate`, so a 4xx/5xx from the server is routed back into
`documentLoadingError` and the user sees the error page or the raw file
as before, not the server's "Internal Server Error".

`kCoreWrapperServesOverHttp` switches back to writing files, which is
also what happens by itself when the port cannot be bound. It is there
to keep that path working while the migration finishes, not as a
runtime option.

Permissions: none needed. A listening socket on loopback takes no
entitlement, and the local network permission from iOS 14 covers the
local subnet and multicast, not 127.0.0.1. App Transport Security does
block plain HTTP, so both Info.plists gain NSAllowsLocalNetworking - the
narrow exception for local addresses, which unlike NSAllowsArbitraryLoads
needs no justification in App Store review.

The conan profile grows a workaround: cpp-httplib resolves host names
through CFHost on Apple platforms and odrcore's CMake links neither
CoreFoundation nor CFNetwork, so its `cli/server` tool fails to link.
The app links both frameworks itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011MhKU2kWm1cPW4GBq9gon5

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e02b7e2c79

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread OpenDocumentReader/CoreWrapper.mm Outdated
Comment thread OpenDocumentReader/DocumentViewController.swift
Two things the review caught.

The readiness probe connected to the port and took an answer as proof
that the server was up. If another app already held 29665, our listen()
failed, its exception was swallowed, and the probe happily shook hands
with the stranger - after which every document got URLs pointing at a
server that had never heard of it, for the rest of the process. The port
is now bind-tested before the server is started, which is what actually
answers "is anyone else here", and listen() coming back sets a flag the
wait loop checks, covering the case where somebody takes the port in
between.

The 4xx/5xx handling applied to every response the web view saw, so a
link in the document leading to a 404, or a frame inside it failing,
replaced the document with the error page. It now only applies to the
main frame and only to URLs odrcore is serving, which `CoreWrapper`
answers for since it owns the host and port.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011MhKU2kWm1cPW4GBq9gon5
@andiwand
andiwand merged commit 506e6a5 into main Jul 26, 2026
4 checks passed
@andiwand
andiwand deleted the core-http-server branch July 26, 2026 10:39
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.

1 participant