Files
WeKnora/internal/models/embedding
toy0116 8955bf1aa8 fix(embedding): OpenAIEmbedder.doRequestWithRetry shadows err · returns (nil, nil) on connection failure · caller nil-derefs and SIGSEGVs
Reproduce:

  1. Run WeKnora pointing OPENAI_BASE_URL at any OpenAI-compatible
     embedding endpoint.
  2. Stop that endpoint.
  3. Issue any RAG-mode chat / knowledge_search query — the pipeline
     reaches chunk_search_parallel → knowledgeBaseService.GetQueryEmbedding
     → OpenAIEmbedder.Embed → OpenAIEmbedder.BatchEmbed.
  4. Observe the entire WeKnora process SIGSEGV instead of returning a
     graceful 5xx.

Stack (real, observed in production):

  panic: runtime error: invalid memory address or nil pointer dereference
  [signal SIGSEGV: segmentation violation code=0x2 addr=0x40]

  internal/models/embedding/openai.go:195 +0x458   BatchEmbed
  internal/models/embedding/openai.go:93  +0xb0    Embed
  internal/application/service/knowledgebase_search.go:35
                                                   GetQueryEmbedding
  internal/application/service/chat_pipeline/search.go:371
                                                   searchByTargets.func1

Root cause — variable shadowing in doRequestWithRetry:

  func (e *OpenAIEmbedder) doRequestWithRetry(...) (*http.Response, error) {
      var resp *http.Response
      var err error                                        // outer err
      ...
      for i := 0; i <= e.maxRetries; i++ {
          ...
          req, err := http.NewRequestWithContext(...)      // ← `:=` shadows
                                                           //   outer `err`
                                                           //   because `req`
                                                           //   is new in
                                                           //   the loop scope
          if err != nil { ... continue }
          ...
          resp, err = e.httpClient.Do(req)                 // writes to the
                                                           // shadowed
                                                           // loop-local err
          if err == nil { return resp, nil }
          logger.Errorf("attempt %d failed: %v", i+1, err) // logs the inner err
      }
      return nil, err                                      // returns OUTER
                                                           // err — never set
  }

When httpClient.Do returns connection errors on every attempt, the loop
logs them faithfully but the outer err stays nil. The function returns
(nil, nil). Caller in BatchEmbed reads:

  resp, err := e.doRequestWithRetry(ctx, jsonData)
  if err != nil { return nil, ... }       // skipped (err is nil)
  if resp.Body != nil {                   // ← line 195 · resp itself is nil
      defer resp.Body.Close()             //   nil pointer dereference
  }                                       //   → SIGSEGV → process dies

Fix — declare `req` separately so the shared `err` variable is not shadowed:

  var req *http.Request
  req, err = http.NewRequestWithContext(...)               // outer err

After the fix, when all retries fail, doRequestWithRetry returns the real
error (e.g. `dial tcp 127.0.0.1:11434: connect: connection refused`),
the caller propagates it as a regular error, the chat pipeline returns a
5xx to the client, and the process keeps running.

Verified by stopping the embedding upstream while WeKnora was serving
queries · process stayed up · returned graceful errors · resumed normal
operation when the upstream came back. Without the patch the same
sequence killed the process within one query.
2026-05-04 23:18:42 +08:00
..