Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -196,12 +196,13 @@ services:
- OPENSEARCH_URL=https://${LANGFLOW_OPENSEARCH_HOST:-${OPENSEARCH_HOST:-opensearch}}:${LANGFLOW_OPENSEARCH_PORT:-${OPENSEARCH_PORT:-9200}}
- OPENSEARCH_INDEX_NAME=${OPENSEARCH_INDEX_NAME:-documents}
- DOCLING_SERVE_URL=${DOCLING_SERVE_URL:-http://host.docker.internal:5001}
- DOCLING_SERVE_VERIFY_SSL=${DOCLING_SERVE_VERIFY_SSL:-true}
- DOCLING_TASK_ID=None
- FILENAME=None
- MIMETYPE=None
- FILESIZE=0
- SELECTED_EMBEDDING_MODEL=${SELECTED_EMBEDDING_MODEL:-text-embedding-3-small}
- LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT=JWT,OPENRAG-QUERY-FILTER,OPENSEARCH_PASSWORD,OPENSEARCH_URL,DOCLING_SERVE_URL,DOCLING_TASK_ID,OWNER,OWNER_NAME,OWNER_EMAIL,CONNECTOR_TYPE,DOCUMENT_ID,SOURCE_URL,ALLOWED_USERS,ALLOWED_GROUPS,ALLOWED_PRINCIPALS,FILENAME,MIMETYPE,FILESIZE,SELECTED_EMBEDDING_MODEL,OPENAI_API_KEY,ANTHROPIC_API_KEY,WATSONX_APIKEY,WATSONX_URL,WATSONX_PROJECT_ID,OLLAMA_BASE_URL,OPENSEARCH_INDEX_NAME,OPENRAG_INGEST_URL,OPENRAG_INGEST_TOKEN,OPENRAG_INGEST_RUN_ID,OPENRAG_INGEST_BATCH_SIZE
- LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT=JWT,OPENRAG-QUERY-FILTER,OPENSEARCH_PASSWORD,OPENSEARCH_URL,DOCLING_SERVE_URL,DOCLING_SERVE_VERIFY_SSL,DOCLING_TASK_ID,OWNER,OWNER_NAME,OWNER_EMAIL,CONNECTOR_TYPE,DOCUMENT_ID,SOURCE_URL,ALLOWED_USERS,ALLOWED_GROUPS,ALLOWED_PRINCIPALS,FILENAME,MIMETYPE,FILESIZE,SELECTED_EMBEDDING_MODEL,OPENAI_API_KEY,ANTHROPIC_API_KEY,WATSONX_APIKEY,WATSONX_URL,WATSONX_PROJECT_ID,OLLAMA_BASE_URL,OPENSEARCH_INDEX_NAME,OPENRAG_INGEST_URL,OPENRAG_INGEST_TOKEN,OPENRAG_INGEST_RUN_ID,OPENRAG_INGEST_BATCH_SIZE
- LANGFLOW_LOG_LEVEL=DEBUG
- LANGFLOW_WORKERS=${LANGFLOW_WORKERS:-1}
- LANGFLOW_AUTO_LOGIN=${LANGFLOW_AUTO_LOGIN}
Expand Down
26 changes: 24 additions & 2 deletions flows/components/docling_remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,15 @@ class DoclingRemoteComponent(BaseFileComponent):
),
input_types=["Message"],
),
StrInput(
name="verify_ssl",
display_name="Verify SSL",
info="Whether to verify SSL certificates for Docling Serve.",
value="DOCLING_SERVE_VERIFY_SSL",
load_from_db=True,
required=False,
advanced=True,
),
Comment on lines +136 to +144

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Default unknown verify_ssl values to secure True.

This parser disables certificate verification for any string outside a tiny truthy allowlist. Because verify_ssl is a free-form StrInput, a typo, extra whitespace, blank value, or unresolved placeholder will silently turn TLS verification off for both Docling client paths.

Suggested fix
     def _get_verify_ssl(self) -> bool:
         """Determine whether to verify SSL certificates for Docling Serve.
 
         Returns:
             bool: True if SSL verification should be enforced, False otherwise.
         """
-        verify = getattr(self, "verify_ssl", "true")
+        verify = getattr(self, "verify_ssl", True)
         if isinstance(verify, bool):
             return verify
         if isinstance(verify, str):
-            return verify.lower() in ("true", "1", "yes")
+            normalized = verify.strip().lower()
+            if normalized in {"false", "0", "no"}:
+                return False
+            return True
         return True

Also applies to: 293-304

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@flows/components/docling_remote.py` around lines 136 - 144, The verify_ssl
StrInput currently treats any string not in a tiny truthy list as False, which
disables TLS on typos/blank values; update the parsing/consumption of the
StrInput named "verify_ssl" so you first trim and lowercase the value, treat
empty/missing/unresolved placeholders as secure (True), and only accept an
explicit truthy allowlist (e.g. "true","1","yes","y") as True while treating
everything else as True by default if ambiguous — then convert that boolean and
pass it to both Docling client creation sites (the two places where verify_ssl
is read/used) so TLS verification defaults to enabled unless the user
intentionally sets a valid false token.

]

outputs = [
Expand Down Expand Up @@ -281,6 +290,19 @@ def _poll_and_fetch_result(
self.log(f"Error validating the document. {e}")
return None

def _get_verify_ssl(self) -> bool:
"""Determine whether to verify SSL certificates for Docling Serve.

Returns:
bool: True if SSL verification should be enforced, False otherwise.
"""
verify = getattr(self, "verify_ssl", "true")
if isinstance(verify, bool):
return verify
if isinstance(verify, str):
return verify.lower() in ("true", "1", "yes")
return True

def _process_task_id(self) -> list[Data]:
"""Process an existing task by polling for status and retrieving results.

Expand All @@ -290,7 +312,7 @@ def _process_task_id(self) -> list[Data]:
transformed_url = transform_localhost_url(self.api_url)
base_url = f"{transformed_url}/v1"

with httpx.Client(headers=self._process_headers()) as client:
with httpx.Client(headers=self._process_headers(), verify=self._get_verify_ssl()) as client:
result = self._poll_and_fetch_result(client, base_url, self.task_id)
return [result] if result else []

Expand Down Expand Up @@ -335,7 +357,7 @@ def _convert_document(

processed_data: list[Data | None] = []
with (
httpx.Client(headers=self._process_headers()) as client,
httpx.Client(headers=self._process_headers(), verify=self._get_verify_ssl()) as client,
ThreadPoolExecutor(max_workers=self.max_concurrency) as executor,
):
futures: list[tuple[int, Future]] = []
Expand Down
26 changes: 24 additions & 2 deletions flows/ingestion_flow.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions kubernetes/helm/openrag/templates/backend/backend-dotenv.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@ stringData:
{{- if and .Values.global.docling.scheme .Values.global.docling.host .Values.global.docling.port }}
DOCLING_SERVE_URL={{ printf "%s://%s:%v" .Values.global.docling.scheme .Values.global.docling.host .Values.global.docling.port | quote }}
{{- end }}
{{- if hasKey .Values.global.docling "verifySsl" }}
DOCLING_SERVE_VERIFY_SSL={{ ternary "true" "false" .Values.global.docling.verifySsl | quote }}
{{- else }}
DOCLING_SERVE_VERIFY_SSL="true"
{{- end }}
Comment on lines +153 to +157

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Compare DOCLING_SERVE_VERIFY_SSL defaults in Helm vs operator

echo "=== Helm backend-dotenv default when verifySsl key absent ==="
rg -n 'DOCLING_SERVE_VERIFY_SSL.*"true"' kubernetes/helm/openrag/templates/backend/

echo ""
echo "=== Operator env.go default for DOCLING_SERVE_VERIFY_SSL ==="
rg -n 'DOCLING_SERVE_VERIFY_SSL.*"false"' kubernetes/operator/internal/controller/env.go

Repository: langflow-ai/openrag

Length of output: 541


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Helm backend-dotenv.yaml (around 148-160) ==="
nl -ba kubernetes/helm/openrag/templates/backend/backend-dotenv.yaml | sed -n '140,170p'

echo
echo "=== Operator env.go (around 70-110) ==="
nl -ba kubernetes/operator/internal/controller/env.go | sed -n '60,115p'

echo
echo "=== Operator env.go (around 135-170) ==="
nl -ba kubernetes/operator/internal/controller/env.go | sed -n '130,180p'

Repository: langflow-ai/openrag

Length of output: 174


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Helm backend-dotenv.yaml (around 148-160) ==="
cat -n kubernetes/helm/openrag/templates/backend/backend-dotenv.yaml | sed -n '145,165p'

echo
echo "=== Operator env.go (around 70-115) ==="
cat -n kubernetes/operator/internal/controller/env.go | sed -n '70,115p'

echo
echo "=== Operator env.go (around 135-180) ==="
cat -n kubernetes/operator/internal/controller/env.go | sed -n '135,180p'

Repository: langflow-ai/openrag

Length of output: 5931


🏁 Script executed:

rg -n "DOCLING_SERVE_VERIFY_SSL" .

Repository: langflow-ai/openrag

Length of output: 18841


Align DOCLING_SERVE_VERIFY_SSL default between Helm and the operator
Helm defaults DOCLING_SERVE_VERIFY_SSL to "true" when global.docling.verifySsl is absent (kubernetes/helm/openrag/templates/backend/backend-dotenv.yaml lines 153-157; also kubernetes/helm/openrag/templates/langflow/langflow-dotenv.yaml lines 111-113). The operator defaults it to "false" in both env var sets (kubernetes/operator/internal/controller/env.go lines 87 and 149). Since src/config/settings.py reads this env var and src/services/docling_service.py uses it as httpx’s verify flag, SSL verification will differ by deployment mode unless users set the value explicitly—update one side so the defaults match (or document the intended behavioral difference).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@kubernetes/helm/openrag/templates/backend/backend-dotenv.yaml` around lines
153 - 157, Helm and the operator disagree on the DOCLING_SERVE_VERIFY_SSL
default—Helm sets it to "true" while the operator sets "false"—so update the
operator to match the secure Helm default: in
kubernetes/operator/internal/controller/env.go change both places where
DOCLING_SERVE_VERIFY_SSL is set to default "false" to instead default to "true"
(these are the two env var definitions that populate the operator's pod env);
ensure no other code branches override it (note src/config/settings.py reads
this env var and src/services/docling_service.py uses it as httpx verify).

# Langflow auth
{{- if .Values.langflow.auth.superuser }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ stringData:
{{- if and .Values.global.docling.scheme .Values.global.docling.host .Values.global.docling.port }}
DOCLING_SERVE_URL={{ printf "%s://%s:%v" .Values.global.docling.scheme .Values.global.docling.host .Values.global.docling.port | quote }}
{{- end }}
{{- if hasKey .Values.global.docling "verifySsl" }}
DOCLING_SERVE_VERIFY_SSL={{ ternary "true" "false" .Values.global.docling.verifySsl | quote }}
{{- else }}
DOCLING_SERVE_VERIFY_SSL="true"
{{- end }}

# OpenSearch connection (for flows)
OPENSEARCH_HOST={{ include "openrag.langflow.opensearch.host" . | quote }}
Expand Down
3 changes: 2 additions & 1 deletion kubernetes/helm/openrag/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ global:
host: docling-serve.docling.svc.cluster.local
port: 5001
scheme: "http"
verifySsl: false

# Shared OAuth credentials (same across all tenants)
oauth:
Expand Down Expand Up @@ -179,7 +180,7 @@ langflow:
host: "https://cloud.langfuse.com"

# Variables to expose to flows
variablesToGetFromEnvironment: "JWT,OPENRAG-QUERY-FILTER,OPENSEARCH_PASSWORD,OPENSEARCH_URL,OPENSEARCH_INDEX_NAME,DOCLING_SERVE_URL,DOCLING_TASK_ID,OWNER,OWNER_NAME,OWNER_EMAIL,CONNECTOR_TYPE,DOCUMENT_ID,SOURCE_URL,ALLOWED_USERS,ALLOWED_GROUPS,ALLOWED_PRINCIPALS,FILENAME,MIMETYPE,FILESIZE,SELECTED_EMBEDDING_MODEL,OPENAI_API_KEY,ANTHROPIC_API_KEY,WATSONX_APIKEY,WATSONX_URL,WATSONX_PROJECT_ID,OLLAMA_BASE_URL,OPENRAG_INGEST_URL,OPENRAG_INGEST_TOKEN,OPENRAG_INGEST_RUN_ID,OPENRAG_INGEST_BATCH_SIZE"
variablesToGetFromEnvironment: "JWT,OPENRAG-QUERY-FILTER,OPENSEARCH_PASSWORD,OPENSEARCH_URL,OPENSEARCH_INDEX_NAME,DOCLING_SERVE_URL,DOCLING_SERVE_VERIFY_SSL,DOCLING_TASK_ID,OWNER,OWNER_NAME,OWNER_EMAIL,CONNECTOR_TYPE,DOCUMENT_ID,SOURCE_URL,ALLOWED_USERS,ALLOWED_GROUPS,ALLOWED_PRINCIPALS,FILENAME,MIMETYPE,FILESIZE,SELECTED_EMBEDDING_MODEL,OPENAI_API_KEY,ANTHROPIC_API_KEY,WATSONX_APIKEY,WATSONX_URL,WATSONX_PROJECT_ID,OLLAMA_BASE_URL,OPENRAG_INGEST_URL,OPENRAG_INGEST_TOKEN,OPENRAG_INGEST_RUN_ID,OPENRAG_INGEST_BATCH_SIZE"

# Probes
livenessProbe:
Expand Down
4 changes: 4 additions & 0 deletions kubernetes/operator/api/v1alpha1/openrag_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,10 @@ type DoclingSpec struct {
// +optional
// +kubebuilder:default="http"
Scheme string `json:"scheme,omitempty"`

// +optional
// +kubebuilder:default=false
VerifySsl *bool `json:"verifySsl,omitempty"`
}

// DoclingServeSpec configures the Docling serve component (API server).
Expand Down
7 changes: 6 additions & 1 deletion kubernetes/operator/api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions kubernetes/operator/config/crd/bases/openr.ag_openrags.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2184,6 +2184,9 @@ spec:
scheme:
default: http
type: string
verifySsl:
default: false
type: boolean
required:
- host
type: object
Expand Down
12 changes: 7 additions & 5 deletions kubernetes/operator/internal/controller/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func NewEnvVarManager() *EnvVarManager {
"LANGFLOW_DATABASE_URL": "sqlite:////app/data/langflow.db",

// Variables to expose to Langflow components
"LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT": "JWT,OPENRAG_QUERY_FILTER,OPENSEARCH_PASSWORD,OPENSEARCH_URL,OPENSEARCH_INDEX_NAME,DOCLING_SERVE_URL,DOCLING_TASK_ID,OWNER,OWNER_NAME,OWNER_EMAIL,CONNECTOR_TYPE,DOCUMENT_ID,SOURCE_URL,ALLOWED_USERS,ALLOWED_GROUPS,ALLOWED_PRINCIPALS,FILENAME,MIMETYPE,FILESIZE,SELECTED_EMBEDDING_MODEL,OPENAI_API_KEY,ANTHROPIC_API_KEY,WATSONX_API_KEY,WATSONX_ENDPOINT,WATSONX_PROJECT_ID,OLLAMA_BASE_URL,OPENRAG_INGEST_URL,OPENRAG_INGEST_TOKEN,OPENRAG_INGEST_RUN_ID,OPENRAG_INGEST_BATCH_SIZE",
"LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT": "JWT,OPENRAG_QUERY_FILTER,OPENSEARCH_PASSWORD,OPENSEARCH_URL,OPENSEARCH_INDEX_NAME,DOCLING_SERVE_URL,DOCLING_SERVE_VERIFY_SSL,DOCLING_TASK_ID,OWNER,OWNER_NAME,OWNER_EMAIL,CONNECTOR_TYPE,DOCUMENT_ID,SOURCE_URL,ALLOWED_USERS,ALLOWED_GROUPS,ALLOWED_PRINCIPALS,FILENAME,MIMETYPE,FILESIZE,SELECTED_EMBEDDING_MODEL,OPENAI_API_KEY,ANTHROPIC_API_KEY,WATSONX_API_KEY,WATSONX_ENDPOINT,WATSONX_PROJECT_ID,OLLAMA_BASE_URL,OPENRAG_INGEST_URL,OPENRAG_INGEST_TOKEN,OPENRAG_INGEST_RUN_ID,OPENRAG_INGEST_BATCH_SIZE",

// Authentication and user management
"LANGFLOW_SKIP_AUTH_AUTO_LOGIN": "true",
Expand Down Expand Up @@ -83,7 +83,8 @@ func NewEnvVarManager() *EnvVarManager {
"OPENSEARCH_INDEX_NAME": "None",

// Docling defaults (for variables in LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT)
"DOCLING_SERVE_URL": "None",
"DOCLING_SERVE_URL": "None",
"DOCLING_SERVE_VERIFY_SSL": "false",

// Provider API keys (defaults to None, overridden by CR spec)
"OPENAI_API_KEY": "None",
Expand Down Expand Up @@ -142,9 +143,10 @@ func NewEnvVarManager() *EnvVarManager {
"EMBEDDING_MODEL": "",
"EMBEDDING_PROVIDER": "",

"WATSONX_API_KEY": "",
"WATSONX_ENDPOINT": "",
"WATSONX_PROJECT_ID": "",
"WATSONX_API_KEY": "",
"WATSONX_ENDPOINT": "",
"WATSONX_PROJECT_ID": "",
"DOCLING_SERVE_VERIFY_SSL": "false",
},
DefaultOpenRagFEEnvVars: map[string]string{
// Frontend environment variables will be added here
Expand Down
3 changes: 2 additions & 1 deletion kubernetes/operator/internal/controller/env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ func TestEnvVarManager_EnsureRequiredEnvVars_Integration(t *testing.T) {

// Parse the required variables list
requiredVars := []string{"JWT", "OPENRAG_QUERY_FILTER", "OPENSEARCH_PASSWORD", "OPENSEARCH_URL",
"OPENSEARCH_INDEX_NAME", "DOCLING_SERVE_URL", "DOCLING_TASK_ID", "OWNER", "OWNER_NAME",
"OPENSEARCH_INDEX_NAME", "DOCLING_SERVE_URL", "DOCLING_SERVE_VERIFY_SSL", "DOCLING_TASK_ID", "OWNER", "OWNER_NAME",
"OWNER_EMAIL", "CONNECTOR_TYPE", "DOCUMENT_ID", "SOURCE_URL", "ALLOWED_USERS",
"ALLOWED_GROUPS", "FILENAME", "MIMETYPE", "FILESIZE", "SELECTED_EMBEDDING_MODEL",
"OPENAI_API_KEY", "ANTHROPIC_API_KEY", "WATSONX_API_KEY", "WATSONX_ENDPOINT",
Expand All @@ -470,6 +470,7 @@ func TestEnvVarManager_EnsureRequiredEnvVars_Integration(t *testing.T) {
assert.Equal(t, "None", envVars["OPENSEARCH_URL"], "OPENSEARCH_URL should have default 'None'")
assert.Equal(t, "None", envVars["OPENSEARCH_INDEX_NAME"], "OPENSEARCH_INDEX_NAME should have default 'None'")
assert.Equal(t, "None", envVars["DOCLING_SERVE_URL"], "DOCLING_SERVE_URL should have default 'None'")
assert.Equal(t, "false", envVars["DOCLING_SERVE_VERIFY_SSL"], "DOCLING_SERVE_VERIFY_SSL should have default 'false'")
}

func TestEnvVarManager_EnsureRequiredEnvVars_CustomList(t *testing.T) {
Expand Down
6 changes: 6 additions & 0 deletions kubernetes/operator/internal/controller/openrag_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,9 @@ func (r *OpenRAGReconciler) buildBackendEnv(ctx context.Context, o *openragv1alp
port = 5001
}
envVars["DOCLING_SERVE_URL"] = fmt.Sprintf("%s://%s:%d", scheme, d.Host, port)
if d.VerifySsl != nil {
envVars["DOCLING_SERVE_VERIFY_SSL"] = strconv.FormatBool(*d.VerifySsl)
}
}

// Convert map to .env file format
Expand Down Expand Up @@ -568,6 +571,9 @@ func (r *OpenRAGReconciler) buildLangflowEnv(ctx context.Context, o *openragv1al
port = 5001
}
envVars["DOCLING_SERVE_URL"] = fmt.Sprintf("%s://%s:%d", scheme, d.Host, port)
if d.VerifySsl != nil {
envVars["DOCLING_SERVE_VERIFY_SSL"] = strconv.FormatBool(*d.VerifySsl)
}
}

// Ensure all variables in LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT exist with at least "None" value
Expand Down
3 changes: 3 additions & 0 deletions src/services/langflow_file_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import httpx

from config.settings import (
DOCLING_SERVE_VERIFY_SSL,
LANGFLOW_INGEST_CALLBACK_BATCH_SIZE,
LANGFLOW_INGEST_FLOW_ID,
LANGFLOW_URL_INGEST_FLOW_ID,
Expand Down Expand Up @@ -434,6 +435,7 @@ async def run_ingestion_flow(
"X-Langflow-Global-Var-DOCLING_TASK_ID": str(docling_task_id)
if docling_task_id
else "",
"X-Langflow-Global-Var-DOCLING_SERVE_VERIFY_SSL": str(DOCLING_SERVE_VERIFY_SSL).lower(),
}

# Serialize ACL lists as JSON strings for Langflow global vars
Expand Down Expand Up @@ -608,6 +610,7 @@ async def run_url_ingestion_flow(
"X-Langflow-Global-Var-FILENAME": str(docs_url),
"X-Langflow-Global-Var-MIMETYPE": "text/html",
"X-Langflow-Global-Var-FILESIZE": "0",
"X-Langflow-Global-Var-DOCLING_SERVE_VERIFY_SSL": str(DOCLING_SERVE_VERIFY_SSL).lower(),
}
ingest_token, ingest_run_id = self._configure_ingest_callback(
document_id=resolved_document_id,
Expand Down
Loading