feat(opensearch): Add BASE_URL config to support reverse proxy deployments

This commit is contained in:
Willie Zutz
2025-05-08 20:58:04 -06:00
parent d839769d7e
commit 85605fe166
5 changed files with 81 additions and 7 deletions

View File

@ -140,6 +140,37 @@ For more details, check out the full documentation [here](https://github.com/Itz
Perplexica runs on Next.js and handles all API requests. It works right away on the same network and stays accessible even with port forwarding.
### Running Behind a Reverse Proxy
When running Perplexica behind a reverse proxy (like Nginx, Apache, or Traefik), follow these steps to ensure proper functionality:
1. **Configure the BASE_URL setting**:
- In `config.toml`, set the `BASE_URL` parameter under the `[GENERAL]` section to your public-facing URL (e.g., `https://perplexica.yourdomain.com`)
2. **Ensure proper headers forwarding**:
- Your reverse proxy should forward the following headers:
- `X-Forwarded-Host`
- `X-Forwarded-Proto`
- `X-Forwarded-Port` (if using non-standard ports)
3. **Example Nginx configuration**:
```nginx
server {
listen 80;
server_name perplexica.yourdomain.com;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
}
}
```
This ensures that OpenSearch descriptions, browser integrations, and all URLs work properly when accessing Perplexica through your reverse proxy.
## One-Click Deployment
[![Deploy to Sealos](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://usw.sealos.io/?openapp=system-template%3FtemplateName%3Dperplexica)

View File

@ -1,6 +1,7 @@
[GENERAL]
SIMILARITY_MEASURE = "cosine" # "cosine" or "dot"
KEEP_ALIVE = "5m" # How long to keep Ollama models loaded into memory. (Instead of using -1 use "-1m")
BASE_URL = "" # Optional. When set, overrides detected URL for OpenSearch and other public URLs
[MODELS.OPENAI]
API_KEY = ""

View File

@ -1,5 +1,6 @@
import {
getAnthropicApiKey,
getBaseUrl,
getCustomOpenaiApiKey,
getCustomOpenaiApiUrl,
getCustomOpenaiModelName,
@ -60,6 +61,7 @@ export const GET = async (req: Request) => {
config['customOpenaiApiUrl'] = getCustomOpenaiApiUrl();
config['customOpenaiApiKey'] = getCustomOpenaiApiKey();
config['customOpenaiModelName'] = getCustomOpenaiModelName();
config['baseUrl'] = getBaseUrl();
return Response.json({ ...config }, { status: 200 });
} catch (err) {

View File

@ -1,11 +1,10 @@
import { NextResponse } from 'next/server';
import { getBaseUrl } from '@/lib/config';
export async function GET(request: Request) {
// Get the host from the request
const url = new URL(request.url);
const origin = url.origin;
// Create the OpenSearch XML with the correct origin
/**
* Creates an OpenSearch XML response with the given origin URL
*/
function generateOpenSearchResponse(origin: string): NextResponse {
const opensearchXml = `<?xml version="1.0" encoding="utf-8"?>
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">
<ShortName>Perplexica</ShortName>
@ -17,10 +16,48 @@ export async function GET(request: Request) {
<Url type="application/opensearchdescription+xml" rel="self" template="${origin}/api/opensearch"/>
</OpenSearchDescription>`;
// Return the XML with the correct content type
return new NextResponse(opensearchXml, {
headers: {
'Content-Type': 'application/opensearchdescription+xml',
},
});
}
export async function GET(request: Request) {
// Check if a BASE_URL is explicitly configured
const configBaseUrl = getBaseUrl();
// If BASE_URL is configured, use it, otherwise detect from request
if (configBaseUrl) {
// Remove any trailing slashes for consistency
let origin = configBaseUrl.replace(/\/+$/, '');
return generateOpenSearchResponse(origin);
}
// Detect the correct origin, taking into account reverse proxy headers
const url = new URL(request.url);
let origin = url.origin;
// Extract headers
const headers = Object.fromEntries(request.headers);
// Check for X-Forwarded-Host and related headers to handle reverse proxies
if (headers['x-forwarded-host']) {
// Determine protocol: prefer X-Forwarded-Proto, fall back to original or https
const protocol = headers['x-forwarded-proto'] || url.protocol.replace(':', '');
// Build the correct public-facing origin
origin = `${protocol}://${headers['x-forwarded-host']}`;
// Handle non-standard ports if specified in X-Forwarded-Port
if (headers['x-forwarded-port']) {
const port = headers['x-forwarded-port'];
// Don't append standard ports (80 for HTTP, 443 for HTTPS)
if (!((protocol === 'http' && port === '80') || (protocol === 'https' && port === '443'))) {
origin = `${origin}:${port}`;
}
}
}
// Generate and return the OpenSearch response
return generateOpenSearchResponse(origin);
}

View File

@ -15,6 +15,7 @@ interface Config {
GENERAL: {
SIMILARITY_MEASURE: string;
KEEP_ALIVE: string;
BASE_URL?: string;
};
MODELS: {
OPENAI: {
@ -70,6 +71,8 @@ export const getSimilarityMeasure = () =>
export const getKeepAlive = () => loadConfig().GENERAL.KEEP_ALIVE;
export const getBaseUrl = () => loadConfig().GENERAL.BASE_URL;
export const getOpenaiApiKey = () => loadConfig().MODELS.OPENAI.API_KEY;
export const getGroqApiKey = () => loadConfig().MODELS.GROQ.API_KEY;