feat(workflow): enhance HTTP request node with curl debugging support
This commit is contained in:
@@ -137,6 +137,7 @@ class HttpErrorDefaultTemplate(BaseModel):
|
|||||||
description="HTTP response body",
|
description="HTTP response body",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class HttpErrorHandleConfig(BaseModel):
|
class HttpErrorHandleConfig(BaseModel):
|
||||||
method: HttpErrorHandle = Field(
|
method: HttpErrorHandle = Field(
|
||||||
default=HttpErrorHandle.NONE,
|
default=HttpErrorHandle.NONE,
|
||||||
|
|||||||
@@ -167,8 +167,7 @@ class HttpRequestNode(BaseNode):
|
|||||||
"status_code": VariableType.NUMBER,
|
"status_code": VariableType.NUMBER,
|
||||||
"headers": VariableType.OBJECT,
|
"headers": VariableType.OBJECT,
|
||||||
"files": VariableType.ARRAY_FILE,
|
"files": VariableType.ARRAY_FILE,
|
||||||
"output": VariableType.STRING,
|
"output": VariableType.STRING
|
||||||
"curl": VariableType.STRING
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def _build_timeout(self) -> Timeout:
|
def _build_timeout(self) -> Timeout:
|
||||||
@@ -256,13 +255,9 @@ class HttpRequestNode(BaseNode):
|
|||||||
case HttpContentType.NONE:
|
case HttpContentType.NONE:
|
||||||
return {}
|
return {}
|
||||||
case HttpContentType.JSON:
|
case HttpContentType.JSON:
|
||||||
rendered_body = self._render_template(
|
content["json"] = json.loads(self._render_template(
|
||||||
self.typed_config.body.data, variable_pool
|
))
|
||||||
).strip()
|
|
||||||
if not rendered_body:
|
|
||||||
content["json"] = {}
|
|
||||||
else:
|
|
||||||
content["json"] = json.loads(rendered_body)
|
|
||||||
case HttpContentType.FROM_DATA:
|
case HttpContentType.FROM_DATA:
|
||||||
data = {}
|
data = {}
|
||||||
files = []
|
files = []
|
||||||
@@ -330,65 +325,6 @@ class HttpRequestNode(BaseNode):
|
|||||||
case _:
|
case _:
|
||||||
raise RuntimeError(f"HttpRequest method not supported: {self.typed_config.method}")
|
raise RuntimeError(f"HttpRequest method not supported: {self.typed_config.method}")
|
||||||
|
|
||||||
def _generate_curl_command(
|
|
||||||
self,
|
|
||||||
variable_pool: VariablePool,
|
|
||||||
url: str,
|
|
||||||
headers: dict[str, str],
|
|
||||||
params: dict[str, str],
|
|
||||||
content: dict[str, Any]
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
Generate equivalent curl command for debugging.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
variable_pool: Variable Pool
|
|
||||||
url: Rendered URL
|
|
||||||
headers: Request headers
|
|
||||||
params: Query parameters
|
|
||||||
content: Request body content
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Curl command string
|
|
||||||
"""
|
|
||||||
# Start with curl command
|
|
||||||
curl_parts = ["curl"]
|
|
||||||
|
|
||||||
# Add HTTP method
|
|
||||||
method = self.typed_config.method.value
|
|
||||||
if method != "GET":
|
|
||||||
curl_parts.append(f"-X {method}")
|
|
||||||
|
|
||||||
# Add URL with query parameters
|
|
||||||
if params:
|
|
||||||
param_str = "&".join([f"{k}={v}" for k, v in params.items()])
|
|
||||||
full_url = f"{url}?{param_str}" if "?" not in url else f"{url}&{param_str}"
|
|
||||||
else:
|
|
||||||
full_url = url
|
|
||||||
|
|
||||||
curl_parts.append(f"'{full_url}'")
|
|
||||||
|
|
||||||
# Add headers
|
|
||||||
for key, value in headers.items():
|
|
||||||
curl_parts.append(f"-H '{key}: {value}'")
|
|
||||||
|
|
||||||
# Add body based on content type
|
|
||||||
if "json" in content:
|
|
||||||
json_body = json.dumps(content["json"], ensure_ascii=False)
|
|
||||||
curl_parts.append(f"-d '{json_body}'")
|
|
||||||
elif "data" in content and "files" not in content:
|
|
||||||
# Form data
|
|
||||||
if isinstance(content["data"], dict):
|
|
||||||
for key, value in content["data"].items():
|
|
||||||
curl_parts.append(f"-F '{key}={value}'")
|
|
||||||
elif "content" in content:
|
|
||||||
# Raw content
|
|
||||||
curl_parts.append(f"-d '{content['content']}'")
|
|
||||||
elif "files" in content:
|
|
||||||
curl_parts.append("# Note: This request includes file uploads")
|
|
||||||
|
|
||||||
return " \\\n ".join(curl_parts)
|
|
||||||
|
|
||||||
async def execute(self, state: WorkflowState, variable_pool: VariablePool) -> dict | str:
|
async def execute(self, state: WorkflowState, variable_pool: VariablePool) -> dict | str:
|
||||||
"""
|
"""
|
||||||
Execute the HTTP request node.
|
Execute the HTTP request node.
|
||||||
@@ -407,17 +343,13 @@ class HttpRequestNode(BaseNode):
|
|||||||
- str: Branch identifier (e.g. "ERROR") when branching is enabled
|
- str: Branch identifier (e.g. "ERROR") when branching is enabled
|
||||||
"""
|
"""
|
||||||
self.typed_config = HttpRequestNodeConfig(**self.config)
|
self.typed_config = HttpRequestNodeConfig(**self.config)
|
||||||
|
|
||||||
# Build request components
|
# Build request components
|
||||||
headers = self._build_header(variable_pool) | self._build_auth(variable_pool)
|
headers = self._build_header(variable_pool) | self._build_auth(variable_pool)
|
||||||
params = self._build_params(variable_pool)
|
params = self._build_params(variable_pool)
|
||||||
content = await self._build_content(variable_pool)
|
content = await self._build_content(variable_pool)
|
||||||
url = self._render_template(self.typed_config.url, variable_pool)
|
url = self._render_template(self.typed_config.url, variable_pool)
|
||||||
|
|
||||||
# Generate curl command for debugging
|
|
||||||
curl_command = self._generate_curl_command(variable_pool, url, headers, params, content)
|
|
||||||
logger.info(f"Node {self.node_id}: Generated curl command:\n{curl_command}")
|
|
||||||
|
|
||||||
async with httpx.AsyncClient(
|
async with httpx.AsyncClient(
|
||||||
verify=self.typed_config.verify_ssl,
|
verify=self.typed_config.verify_ssl,
|
||||||
timeout=self._build_timeout(),
|
timeout=self._build_timeout(),
|
||||||
@@ -441,7 +373,6 @@ class HttpRequestNode(BaseNode):
|
|||||||
status_code=resp.status_code,
|
status_code=resp.status_code,
|
||||||
headers=resp.headers,
|
headers=resp.headers,
|
||||||
files=response.files,
|
files=response.files,
|
||||||
curl=curl_command
|
|
||||||
).model_dump()
|
).model_dump()
|
||||||
except (httpx.HTTPStatusError, httpx.RequestError) as e:
|
except (httpx.HTTPStatusError, httpx.RequestError) as e:
|
||||||
logger.error(f"HTTP request node exception: {e}")
|
logger.error(f"HTTP request node exception: {e}")
|
||||||
@@ -458,10 +389,7 @@ class HttpRequestNode(BaseNode):
|
|||||||
logger.warning(
|
logger.warning(
|
||||||
f"Node {self.node_id}: HTTP request failed, returning default result"
|
f"Node {self.node_id}: HTTP request failed, returning default result"
|
||||||
)
|
)
|
||||||
# Update curl command in default error template
|
return self.typed_config.error_handle.default.model_dump()
|
||||||
error_result = self.typed_config.error_handle.default.model_dump()
|
|
||||||
error_result["curl"] = curl_command
|
|
||||||
return error_result
|
|
||||||
case HttpErrorHandle.BRANCH:
|
case HttpErrorHandle.BRANCH:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Node {self.node_id}: HTTP request failed, switching to error handling branch"
|
f"Node {self.node_id}: HTTP request failed, switching to error handling branch"
|
||||||
@@ -472,6 +400,5 @@ class HttpRequestNode(BaseNode):
|
|||||||
"status_code": 500,
|
"status_code": 500,
|
||||||
"headers": {},
|
"headers": {},
|
||||||
"files": [],
|
"files": [],
|
||||||
"curl": curl_command
|
|
||||||
}
|
}
|
||||||
raise RuntimeError("http request failed")
|
raise RuntimeError("http request failed")
|
||||||
|
|||||||
Reference in New Issue
Block a user