Skip to content

fix(media): resolve public image URLs from request and configurable base#623

Merged
chenyme merged 3 commits into
chenyme:mainfrom
hamburg888:codex/fix-media-url-localhost
Jul 14, 2026
Merged

fix(media): resolve public image URLs from request and configurable base#623
chenyme merged 3 commits into
chenyme:mainfrom
hamburg888:codex/fix-media-url-localhost

Conversation

@hamburg888

Copy link
Copy Markdown
Contributor
## Summary

This PR fixes a production deployment issue where image generation returned localhost media URLs, and adds operator-facing controls so public API base URL resolution can be managed cleanly without restarting the service.

Previously, `POST /v1/images/generations` with `response_format=url` could return:

```json
{
  "data": [
    {
      "url": "http://127.0.0.1:8000/v1/media/images/img_xxx"
    }
  ]
}

That URL is unusable for clients calling the service through LAN IP, reverse proxy, Docker host mapping, or a public domain. Operators had to hand-edit frontend.publicApiBaseURL and restart, which is easy to miss in multi-environment deployments.

This PR solves that in two tightly related commits on the same branch:

  1. 0363483 — derive public image base URL from the incoming request
  2. 9b661ea — make public base URL + request-priority switch configurable in admin Settings

Video generation is intentionally unchanged. Video assets already use upstream CDN URLs such as https://assets.grok.com/..., so they do not suffer from the localhost media URL problem.

Problem

Before

  • Image public URLs were effectively bound to the process-local config value, often defaulting to http://127.0.0.1:8000
  • Docker / reverse-proxy / LAN deployments returned links that only worked on the server itself
  • Operators needed to edit YAML and restart to correct public URLs
  • Admin Settings UI had no way to manage this behavior
  • Docs/system base URL could become stale after config changes

After

  • Image public URLs can be derived automatically from the client request host/proto
  • Operators can pin a fixed public base URL when needed
  • Operators can choose whether request-derived base should take priority
  • Settings changes apply immediately without restart
  • Docs/system endpoint reflects the live configured public base URL

Final user-facing effect

Image generation response

When a client calls the API via a reachable host, the returned media URL uses that host instead of loopback.

Example request:

POST http://192.168.2.103:8000/v1/images/generations

Example response after this PR (with request priority enabled, default):

{
  "created": 1784006494,
  "data": [
    {
      "mime_type": "image/jpeg",
      "revised_prompt": "",
      "url": "http://192.168.2.103:8000/v1/media/images/img_xxx"
    }
  ]
}

If the operator configures a fixed public base and disables request priority, the response becomes:

{
  "data": [
    {
      "url": "https://api.example.com/v1/media/images/img_xxx"
    }
  ]
}

Admin Settings UI

In Settings → Media storage, two new controls are available:

  1. Public API base URL

    • Optional
    • Used for generated image public URLs and docs examples
    • Empty is allowed; empty means “derive from request when possible”
  2. Prefer request base URL

    • Switch, default true
    • Enabled: prefer client-visible host/proto, including reverse-proxy forwarded headers
    • Disabled: prefer the configured public base URL when set

Both fields include short helper text in Chinese and English.

Docs / system info

GET /api/admin/v1/system now returns the live configured publicApiBaseURL.

That means:

  • API docs examples update after Settings save
  • No backend restart is required for the docs base URL to refresh

URL resolution priority

Image public URL base is resolved in this order:

  1. Request-derived base, if preferRequestBaseURL=true and request host is available
  2. Configured publicApiBaseURL, if non-empty
  3. Request-derived base as fallback
  4. http://127.0.0.1:8000 as last-resort fallback

Request-derived base honors:

  • X-Forwarded-Proto
  • X-Forwarded-Host
  • otherwise falls back to request scheme/host

Backend changes

Request-derived public base

  • Added request base derivation in inference HTTP handler
  • Threaded PublicBaseURL through:
    • gateway
    • provider interface
    • web image/chat adapters
    • media PublicImageURL
  • Empty frontend.publicApiBaseURL is now valid configuration

Media URL generation

  • media.Service.PublicImageURL now uses switch-aware priority resolution
  • Added PreferRequestBaseURL to media runtime config
  • Hot-update path reloads both:
    • configured public base
    • request-priority preference

Settings / config model

  • Added hot-editable frontend settings:
    • publicApiBaseURL
    • preferRequestBaseURL
  • Wired through:
    • domain settings model
    • settings application service
    • settings HTTP DTO
    • runtime settings persistence
    • config example YAML
  • Backward compatible with old persisted settings that do not contain the new frontend block
  • preferRequestBaseURL defaults to true

System endpoint

  • /api/admin/v1/system no longer freezes the startup-time public base URL
  • It now reads the live value from settings service after hot updates

Tests

  • Media URL priority matrix tests
  • Settings persist/apply tests for new frontend fields
  • Legacy load compatibility test when frontend block is absent
  • System handler updated for live public base provider
  • Existing config validation tests updated for empty public base support

Frontend changes

Settings form

  • Added fields under Media storage:
    • public API base URL input
    • prefer-request-base-url switch
  • Added field-level helper descriptions via shared SettingsField description support
  • Added frontend validation:
    • empty public base is allowed
    • non-empty value must be absolute http / https
    • rejects credentials / query / hash

API model / mapping

  • Settings DTO extended with:
    • frontend.publicApiBaseURL
    • frontend.preferRequestBaseURL
  • Form ↔ DTO conversion updated
  • Response decoder shape updated

i18n

  • Added Chinese and English labels + help text for both new settings

Why this is one PR, not two

These two commits are one product fix:

  1. make image URLs correct automatically
  2. give operators a first-class way to control that behavior

Splitting them would create incomplete review context and overlapping changes in:

  • media/service.go
  • config model
  • settings plumbing

Out of scope

  • Video generation URL generation is unchanged because videos already use CDN URLs
  • No change to image storage backend itself
  • No change to authentication / client key behavior

Test plan

  • Backend unit tests for media / settings / system / config
  • Frontend tsc -b
  • Settings PUT updates /api/admin/v1/system without restart
  • Local backend restarted with new binary and settings API verified
  • Manual image generation against 127.0.0.1
  • Manual image generation against LAN host such as 192.168.x.x
  • Manual image generation behind reverse proxy with X-Forwarded-*
  • Disable request priority, set fixed public base, verify pinned URLs
  • Clear public base, enable request priority, verify auto-derived URLs
  • Confirm video generation still returns CDN URLs

Commits

  1. 0363483 fix(media): derive image URL base from request when publicApiBaseURL is empty
  2. 9b661ea feat(settings): make public API base URL configurable with request preference
    (https://github.com/chenyme/grok2api/compare/main...hamburg888:codex/fix-media-url-localhost?expand=1)

hamburg888 and others added 3 commits July 14, 2026 16:00
…is empty

When deployed behind a reverse proxy or in Docker, generated image URLs
returned to clients contained the internal loopback address
(http://127.0.0.1:8000/...), making them unusable from the public
network. The operator had to manually set frontend.publicApiBaseURL to
work around this.

This change introduces a two-layer resolution strategy:

1. Request-derived base URL: the HTTP handler now derives the public base
   from the incoming request (honouring X-Forwarded-Proto /
   X-Forwarded-Host) and threads it through the gateway, provider, and
   web adapter layers down to PublicImageURL. This makes URLs correct
   out of the box without any config change.

2. Fallback chain: PublicImageURL resolves the base in priority order:
   request-derived URL -> hot-updatable config publicApiBaseURL ->
   "http://127.0.0.1:8000" last-resort default. The config field is now
   optional (empty is valid); when set it still takes priority over the
   request-derived value, allowing operators to pin a specific public
   endpoint.

Video generation is unaffected because it uses hardcoded CDN URLs
(https://assets.grok.com/...).

Tests updated:
- protocol_test.go: stub signature + imageDataItem calls adjusted for
  the new publicBaseURL parameter.
- service_test.go: PublicImageURL call updated to two-arg signature.
- config_test.go: empty string removed from the invalid publicApiBaseURL
  values now that empty is accepted.
…eference

Expose frontend.publicApiBaseURL and frontend.preferRequestBaseURL through
the admin settings API and UI so operators can control image/public URL
generation without editing YAML or restarting the process.

Resolution priority is now switch-aware:
1. request-derived base when preferRequestBaseURL is enabled
2. configured publicApiBaseURL when set
3. request-derived base as fallback
4. http://127.0.0.1:8000 as last resort

Also hot-update /api/admin/v1/system so docs examples reflect the live
configured base URL after settings changes.
# Conflicts:
#	backend/internal/application/gateway/service.go
#	backend/internal/application/media/service.go
#	frontend/src/shared/i18n/index.ts
@chenyme chenyme merged commit 3b46c25 into chenyme:main Jul 14, 2026
9 checks passed
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.

2 participants