Skip to content

Improve rootPath implementation with fallback strategy and refactoring#3

Merged
DaehoYang merged 2 commits intodevfrom
copilot/improve-rootpath-implementation
Feb 2, 2026
Merged

Improve rootPath implementation with fallback strategy and refactoring#3
DaehoYang merged 2 commits intodevfrom
copilot/improve-rootpath-implementation

Conversation

Copy link
Copy Markdown

Copilot AI commented Feb 2, 2026

What does this PR do?

Addresses instability in rootPath functionality by implementing automatic fallback and eliminating code duplication.

Core Changes:

  • Fallback Strategy: createStaticOrProxyHandler() serves local build when available, proxies to app.opencode.ai otherwise. When rootPath is set, local build is required (no fallback) - throws clear error if missing.

  • Deduplication: Extracted common routing logic into createAppWithRoutes(), eliminating ~40 lines of duplicated setup between rootPath and non-rootPath modes.

  • Test Coverage: Added 9 tests covering special characters, URL normalization edge cases, WebSocket paths, and fallback behavior.

  • Constants: Hardcoded paths moved to APP_DIST_PATH, APP_INDEX_PATH, REMOTE_PROXY_URL.

Behavior Matrix:

Local Build rootPath Result
✅ Exists ❌ No Serves local
❌ Missing ❌ No Proxies remote (new)
✅ Exists ✅ Yes Serves local at path
❌ Missing ✅ Yes Error with guidance

Example:

// Automatically falls back to remote proxy when dist missing
async function createStaticOrProxyHandler() {
  const localAppExists = await Bun.file(APP_INDEX_PATH).exists()
  
  return localAppExists 
    ? { type: "local", handler: serveStatic({ root: APP_DIST_PATH }) }
    : { type: "proxy", handler: async (c) => proxy(`${REMOTE_PROXY_URL}${c.req.path}`, {...}) }
}

No breaking changes. Security measures (XSS prevention, CSP headers) preserved.

How did you verify your code works?

  • Syntax validation via explore agent
  • Added comprehensive test suite (88 lines, 9 tests)
  • Verified backward compatibility by checking existing test structure unchanged
  • Reviewed all modified files for proper integration
Original prompt

목표

기존 rootPath 구현을 개선하여 기존 사용자에게 영향을 주지 않으면서도 안정성과 유지보수성을 높입니다.

주요 개선 사항

1. Fallback 전략 구현 (최우선)

현재 문제:

  • 원격 프록시 기능을 완전히 제거하여 로컬 빌드가 없는 환경에서 실행 불가
  • 개발 환경, 부분 배포, 컨테이너 환경에서 문제 발생 가능

해결 방안:
packages/opencode/src/server/server.ts에 다음 로직 추가:

/**
 * Creates a handler that serves static files locally if available,
 * otherwise falls back to remote proxy
 */
async function createStaticOrProxyHandler() {
  const indexPath = "../app/dist/index.html"
  const indexFile = Bun.file(indexPath)
  const localAppExists = await indexFile.exists()
  
  if (localAppExists) {
    log.info("📦 Serving app from local build (../app/dist)")
    return {
      type: "local" as const,
      handler: serveStatic({ root: "../app/dist" })
    }
  } else {
    log.warn("🌐 Local app build not found, falling back to remote proxy (https://app.opencode.ai)")
    log.warn("   For better performance, build the app: cd packages/app && bun run build")
    
    return {
      type: "proxy" as const,
      handler: async (c: any) => {
        const path = c.req.path
        const response = await proxy(`https://app.opencode.ai${path}`, {
          ...c.req,
          headers: {
            ...c.req.raw.headers,
            host: "app.opencode.ai",
          },
        })
        response.headers.set("Content-Security-Policy", HTML_CSP_HEADER)
        return response
      }
    }
  }
}

rootPath 사용 시 로컬 빌드 강제:

export async function listen(opts: {...}) {
  // rootPath requires local build for reliable routing
  if (opts.rootPath) {
    const localAppExists = await Bun.file("../app/dist/index.html").exists()
    if (!localAppExists) {
      throw new Error(
        "rootPath requires local app build.\n" +
        "Build the app first: cd packages/app && bun run build\n" +
        "Or run without --root-path to use remote proxy."
      )
    }
  }
  
  const { type: serveType, handler: staticHandler } = await createStaticOrProxyHandler()
  
  // ... 기존 로직에서 serveStatic 대신 staticHandler 사용
}

2. server.ts 리팩토링 - 중복 제거

현재 문제:

  • rootPath 유무에 따라 거의 동일한 라우팅 설정이 중복됨
  • 유지보수가 어렵고 실수 가능성 높음

해결 방안:

/**
 * Creates app with common routes to avoid duplication
 */
function createAppWithRoutes(
  indexHandler: (c: any) => Promise<Response>,
  staticHandler: any,
  apiApp: Hono
): Hono {
  return new Hono()
    .route("/", apiApp)
    .get("/", indexHandler)
    .get("/index.html", indexHandler)
    .use("/*", staticHandler)
    .all("/*", indexHandler) as unknown as Hono
}

export async function listen(opts: {...}) {
  // ...
  const apiApp = App()
  const indexHandler = createIndexHandler(_rootPath)
  const { handler: staticHandler } = await createStaticOrProxyHandler()
  
  let baseApp: Hono
  
  if (opts.rootPath) {
    // Mount at rootPath with fallback for absolute asset paths
    const rootedApp = new Hono()
      .basePath(opts.rootPath)
      .route("/", createAppWithRoutes(indexHandler, staticHandler, apiApp))
    
    baseApp = new Hono()
      .route("/", rootedApp)
      .use("/*", staticHandler) // Handle absolute paths like /assets/...
  } else {
    // Standard setup
    baseApp = createAppWithRoutes(indexHandler, staticHandler, apiApp)
  }
  
  // ... rest of the code
}

3. 포괄적인 테스트 추가

packages/opencode/test/server/rootpath.test.ts에 다음 테스트 추가:

describe("Special character handling", () => {
  test("handles URL encoded characters", () => {
    const html = '<html><head></head><body><div id="root"></div></body></html>'
    const result = injectRootPath(html, "/한글/경로")
    
    // Should properly escape in HTML attributes
    expect(result).toContain('data-root-path=')
    // Should safely encode in JavaScript
    expect(result).toContain('window.__OPENCODE__.rootPath')
  })
  
  test("handles spaces and special chars in rootPath", () => {
    const html = '<html><head></head><body><div id="root"></div></body></html>'
    const paths = ["/path with space", "/path-with-dash", "/path_with_underscore", "/path.with.dot"]
    
    for (const path of paths) {
      const result = injectRootPath(html, path)
      expect(result).toContain(JSON.stringify(path))
    }
  })
  
  test("handles paths with query-like characters", () => {
    const maliciousPath = "/proxy?token=abc&key=xyz"
    const html = '<html><head></head><body><div id="root"></div></body></html>'
    const result = injectRootPath(html, maliciousPath)
    
    // Should be safely escaped
    expect(result).toContain(JSON.stringify(maliciousPath))
  })
})

describe("URL normalization edge cases", () => {
  test("handles multiple consecutive slashes", () => {
    expect(normalizeUrl("http://localhost:4096", "///proxy///path///")).toBe(
      "http://localhost:4096/proxy/path/"
    )
  })
  
  test("handles mixed slash patterns", () => {
    expect(normalizeUrl("http://localhost:4096/", "//proxy/path")).toBe(
      "http://localhost:4096/proxy/path"
    )
  })
  
  test("preserv...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

*This pull request was created from Copilot chat.*
>

<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.

Co-authored-by: DaehoYang <129835752+DaehoYang@users.noreply.github.com>
Copilot AI changed the title [WIP] Improve rootPath implementation for better stability and maintainability Improve rootPath implementation with fallback strategy and refactoring Feb 2, 2026
Copilot AI requested a review from DaehoYang February 2, 2026 14:31
@DaehoYang DaehoYang marked this pull request as ready for review February 2, 2026 22:43
@DaehoYang DaehoYang merged commit e106eee into dev Feb 2, 2026
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