/
vshmidt
/
streamlit
Обзор
Документация
Войти
/
vshmidt
/
streamlit
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
frontend/connection/src/utils.ts
255 строк
7 KB
Ken McGrady
Update copyright header to 2026 (#13491)
02 янв 2026, 16:21
Не верифицирован
02 янв 2026, 16:21
374540a
Код
Авторство
О чём код?
/** * Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2026) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { isValidAllowedOrigins, makePath, StreamlitConfig, } from "@streamlit/utils" const FINAL_SLASH_RE = /\/+$/ const INITIAL_SLASH_RE = /^\/+/ /** * Returns true when host has provided sufficient config properties for establishing the * initial websocket connection without waiting for the host-config endpoint response. * * This bypass relies on minimal host configuration provided via window.__streamlit (StreamlitConfig) * * Required fields: * - BACKEND_BASE_URL: string * - HOST_CONFIG.allowedOrigins: non-empty array of non-empty strings * - HOST_CONFIG.useExternalAuthToken: boolean (true or false) * * NOTE: changes to this function must be reflected in the mock in App.test.tsx */ export function isHostConfigBypassEnabled(): boolean { const initialHostConfig = StreamlitConfig.HOST_CONFIG if (!initialHostConfig) { return false } const { allowedOrigins, useExternalAuthToken } = initialHostConfig // Validate required fields using shared validation logic const hasValidBackendBaseUrl = Boolean(StreamlitConfig.BACKEND_BASE_URL) const hasValidAllowedOrigins = isValidAllowedOrigins(allowedOrigins) const hasValidUseExternalAuthToken = typeof useExternalAuthToken === "boolean" return ( hasValidBackendBaseUrl && hasValidAllowedOrigins && hasValidUseExternalAuthToken ) } /** * Return the BaseUriParts for either the given url or the global window */ export function parseUriIntoBaseParts(url?: string): URL { const currentUrl = new URL(url ?? window.location.href) currentUrl.pathname = currentUrl.pathname .replace(FINAL_SLASH_RE, "") .replace(INITIAL_SLASH_RE, "") return currentUrl } // NOTE: In the multipage apps world, there is some ambiguity around whether a // path like "foo/bar" means // * the page "/" at baseUrlPath "foo/bar", or // * the page "/bar" at baseUrlPath "foo". // To resolve this, we just try both possibilities for now, but this leads to // the unfortunate consequence of the initial page load when navigating directly // to a non-main page of an app being slower than navigating to the main page // (as the first attempt at connecting to the server fails the healthcheck). // // We'll want to improve this situation in the near future, but figuring out // the best path forward may be tricky as I wasn't able to come up with an // easy solution covering every deployment scenario. export function getPossibleBaseUris(): Array<URL> { const baseUriParts = parseUriIntoBaseParts(StreamlitConfig.BACKEND_BASE_URL) const { pathname } = baseUriParts if (pathname === "/") { return [baseUriParts] } const parts = pathname.split("/") const possibleBaseUris: Array<URL> = [] while (parts.length > 0) { const newURL = new URL(baseUriParts) newURL.pathname = parts.join("/") possibleBaseUris.push(newURL) parts.pop() } if (possibleBaseUris.length <= 2) { return possibleBaseUris } return possibleBaseUris.slice(0, 2) } /** * Create a ws:// or wss:// URI for the given path. */ export function buildWsUri( { hostname, port, pathname, protocol }: URL, path: string ): string { const wsProtocol = protocol === "https:" ? "wss" : "ws" const fullPath = makePath(pathname, path) return `${wsProtocol}://${hostname}:${port}/${fullPath}` } /** * Serialize data for display in error messages. * Objects are pretty-printed as JSON, primitives are converted to strings. * Returns undefined for null, undefined, or non-serializable types. */ export function serializeForDisplay(data: unknown): string | undefined { if (data === null || data === undefined) { return undefined } switch (typeof data) { case "object": return JSON.stringify(data, null, 2) case "string": case "number": case "boolean": return String(data) default: return undefined } } /** * Custom error class to normalize fetch errors into a consistent structure. * This mimics axios error patterns for compatibility with previous error handling logic. */ export class FetchError extends Error { url: string isTimeout: boolean response?: { status: number statusText: string data: unknown } isNetworkError: boolean constructor( message: string, url: string, options: { isTimeout?: boolean isNetworkError?: boolean response?: { status: number; statusText: string; data: unknown } } = {} ) { super(message) this.name = "FetchError" this.url = url this.isTimeout = options.isTimeout ?? false this.isNetworkError = options.isNetworkError ?? false this.response = options.response } } /** * Fetch with timeout support using AbortController. * Normalizes different error types (timeout, network, HTTP errors) into FetchError. */ export async function fetchWithTimeout( url: string, timeoutMs: number ): Promise<{ data: unknown; url: string }> { const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), timeoutMs) try { const response = await fetch(url, { signal: controller.signal }) clearTimeout(timeoutId) if (!response.ok) { // Server responded with error status let data: unknown try { data = await response.json() } catch { try { data = await response.text() } catch { data = null } } throw new FetchError( `Request failed with status ${response.status}`, url, { response: { status: response.status, statusText: response.statusText, data, }, } ) } // Try JSON first, fall back to text (healthz returns string, host-config returns JSON) // We don't rely on Content-Type since proxies/CDNs may not preserve headers correctly const text = await response.text() try { return { data: JSON.parse(text), url } } catch { return { data: text, url } } } catch (error) { clearTimeout(timeoutId) // Re-throw FetchError as-is if (error instanceof FetchError) { throw error } // Handle AbortController timeout if (error instanceof DOMException && error.name === "AbortError") { throw new FetchError("Connection timed out", url, { isTimeout: true }) } // Handle network errors (TypeError from fetch when network fails) if (error instanceof TypeError) { throw new FetchError(error.message || "Network error", url, { isNetworkError: true, }) } // Generic error throw new FetchError( error instanceof Error ? error.message : "Unknown error", url ) } }