// Изменено: 2026-03-11 // invoke.go — прокси-обработчик для вызова HTTP-триггеров функций. // Маршрут: ANY /fn/{namespace}/{name} и /fn/{namespace}/{name}/** // Не защищён auth-токеном — это публичный эндпоинт для вызова функций. // Проксирует запрос к ClusterIP Service функции внутри кластера: // http://{name}.sless-fn-{namespace}.svc.cluster.local:8080 // Sub-path и query string пробрасываются как есть: // /fn/ns/notes/add?title=x → http://notes.sless-fn-ns.svc.../add?title=x package handler import ( "fmt" "io" "net/http" "strings" "time" "github.com/gorilla/mux" ) // httpClient используется для обращения к функциям внутри кластера. // Таймаут 30s — достаточно для холодного старта функции. var httpClient = &http.Client{Timeout: 30 * time.Second} // hopByHopHeaders — заголовки которые нельзя пробрасывать через прокси (RFC 2616 §13.5.1). // Они управляют соединением между двумя узлами, а не end-to-end. // Особо опасен Transfer-Encoding: если пробросить его, клиент неверно интерпретирует тело. var hopByHopHeaders = map[string]bool{ "Connection": true, "Keep-Alive": true, "Proxy-Authenticate": true, "Proxy-Authorization": true, "Te": true, "Trailers": true, "Transfer-Encoding": true, "Upgrade": true, } // InvokeFunction проксирует входящий запрос к Service функции в кластере. // Namespace выбирается из пути, имя функции — тоже из пути. // Сохраняет метод, тело, Content-Type, sub-path и query string. func (h *Handler) InvokeFunction(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) ns := vars["namespace"] name := vars["name"] // Вычисляем sub-path после /fn/{namespace}/{name} // Например: /fn/default/notes/add → subPath = /add prefix := "/fn/" + ns + "/" + name subPath := strings.TrimPrefix(r.URL.Path, prefix) // Внутренний URL к Service функции (DNS внутри кластера) target := fmt.Sprintf("http://%s.sless-fn-%s.svc.cluster.local:8080%s", name, ns, subPath) // Пробрасываем query string если есть if r.URL.RawQuery != "" { target += "?" + r.URL.RawQuery } // Создаём проксируемый запрос с тем же методом и телом proxyReq, err := http.NewRequestWithContext(r.Context(), r.Method, target, r.Body) if err != nil { h.Log.Error("invoke: failed to create proxy request", "err", err, "ns", ns, "fn", name) writeJSON(w, http.StatusInternalServerError, errResp("failed to create proxy request")) return } // Пробрасываем Content-Type и Content-Length если есть. // Content-Length обязателен: Python BaseHTTPRequestHandler читает тело // ровно столько байт, сколько указано в заголовке; без него body = пусто. if ct := r.Header.Get("Content-Type"); ct != "" { proxyReq.Header.Set("Content-Type", ct) } proxyReq.ContentLength = r.ContentLength resp, err := httpClient.Do(proxyReq) if err != nil { // "no such host" — Service не существует (функция удалена), возвращаем 404. // Это отличается от временной сетевой ошибки: NXDOMAIN строго означает отсутствие записи. if strings.Contains(err.Error(), "no such host") { writeJSON(w, http.StatusNotFound, errResp("function not found")) return } h.Log.Error("invoke: function unreachable", "err", err, "ns", ns, "fn", name, "target", target) writeJSON(w, http.StatusBadGateway, errResp("function unreachable: "+err.Error())) return } defer resp.Body.Close() // Копируем заголовки и статус из ответа функции. // Hop-by-hop заголовки фильтруем: они управляют конкретным TCP-соединением // и не должны пробрасываться через прокси (RFC 2616 §13.5.1). for k, vals := range resp.Header { if hopByHopHeaders[k] { continue } for _, v := range vals { w.Header().Add(k, v) } } w.WriteHeader(resp.StatusCode) _, _ = io.Copy(w, resp.Body) }