├── workflows ├── get_node.png ├── post_node.png ├── rest_node.png ├── image_convertors.png ├── form_post_request_node.png ├── chainable_upload_image_node.png ├── post_node.json ├── form_post_request_node.json ├── rest_node.json └── get_node.json ├── .gitignore ├── pyproject.toml ├── .github └── workflows │ └── publish_action.yml ├── __init__.py ├── image_to_blob_node.py ├── key_value_node.py ├── LICENSE.txt ├── string_replace_node.py ├── image_to_base64_node.py ├── retry_setting_node.py ├── get_node.py ├── form_post_node.py ├── image_list_combiner_node.py ├── post_node.py ├── base_flask_server.py ├── README_zh.md ├── rest_api_node.py └── README.md /workflows/get_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixszeto/ComfyUI-RequestNodes/HEAD/workflows/get_node.png -------------------------------------------------------------------------------- /workflows/post_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixszeto/ComfyUI-RequestNodes/HEAD/workflows/post_node.png -------------------------------------------------------------------------------- /workflows/rest_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixszeto/ComfyUI-RequestNodes/HEAD/workflows/rest_node.png -------------------------------------------------------------------------------- /workflows/image_convertors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixszeto/ComfyUI-RequestNodes/HEAD/workflows/image_convertors.png -------------------------------------------------------------------------------- /workflows/form_post_request_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixszeto/ComfyUI-RequestNodes/HEAD/workflows/form_post_request_node.png -------------------------------------------------------------------------------- /workflows/chainable_upload_image_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixszeto/ComfyUI-RequestNodes/HEAD/workflows/chainable_upload_image_node.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pem 2 | ssl/ 3 | __pycache__/ 4 | *.pyc 5 | *.~ 6 | *.swp 7 | *.bak 8 | *.tmp 9 | *.log 10 | env/ 11 | envs/ 12 | venv/ 13 | environment.yml 14 | .DS_Store 15 | Thumbs.db -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "ComfyUI-RequestNodes" 3 | description = "This is a request node tool designed for making HTTP requests (GET/POST) to APIs and viewing the responses. It is useful for API testing and development." 4 | version = "1.0.6" 5 | license = {file = "LICENSE"} 6 | 7 | [project.urls] 8 | Repository = "https://github.com/felixszeto/ComfyUI-RequestNodes" 9 | # Used by Comfy Registry https://comfyregistry.org 10 | 11 | [tool.comfy] 12 | PublisherId = "felixszeto" 13 | DisplayName = "ComfyUI-RequestNodes" 14 | Icon = "" 15 | -------------------------------------------------------------------------------- /.github/workflows/publish_action.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Comfy registry 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - "pyproject.toml" 9 | 10 | jobs: 11 | publish-node: 12 | name: Publish Custom Node to registry 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Check out code 16 | uses: actions/checkout@v4 17 | - name: Publish Custom Node 18 | uses: Comfy-Org/publish-node-action@main 19 | with: 20 | personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} ## Add your own personal access token to your Github Repository secrets and reference it here. -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from .get_node import GetRequestNode 4 | from .post_node import PostRequestNode 5 | from .key_value_node import KeyValueNode 6 | from .rest_api_node import RestApiNode 7 | from .string_replace_node import StringReplaceNode 8 | from .retry_setting_node import RetrySettingNode 9 | from .form_post_node import FormPostRequestNode 10 | from .image_to_base64_node import ImageToBase64Node 11 | from .image_to_blob_node import ImageToBlobNode 12 | from .image_list_combiner_node import ChainableUploadImage 13 | 14 | 15 | 16 | NODE_CLASS_MAPPINGS = { 17 | "Get Request Node": GetRequestNode, 18 | "Post Request Node": PostRequestNode, 19 | "Form Post Request Node": FormPostRequestNode, 20 | "Rest Api Node": RestApiNode, 21 | "Key/Value Node": KeyValueNode, 22 | "String Replace Node": StringReplaceNode, 23 | "Retry Settings Node": RetrySettingNode, 24 | "Image To Base64 Node": ImageToBase64Node, 25 | "Image To Blob Node": ImageToBlobNode, 26 | "Chainable Upload Image": ChainableUploadImage, 27 | } -------------------------------------------------------------------------------- /image_to_blob_node.py: -------------------------------------------------------------------------------- 1 | import io 2 | import torch 3 | import numpy as np 4 | from PIL import Image 5 | 6 | class ImageToBlobNode: 7 | def __init__(self): 8 | pass 9 | 10 | @classmethod 11 | def INPUT_TYPES(cls): 12 | return { 13 | "required": { 14 | "image": ("IMAGE",), 15 | }, 16 | } 17 | 18 | RETURN_TYPES = ("BYTES",) 19 | FUNCTION = "convert_image_to_blob" 20 | CATEGORY = "RequestNode/Converters" 21 | 22 | def convert_image_to_blob(self, image): 23 | # Convert tensor to PIL Image 24 | i = 255. * image.cpu().numpy().squeeze() 25 | img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) 26 | 27 | # Save image to a byte buffer 28 | buffer = io.BytesIO() 29 | img.save(buffer, format="PNG") 30 | 31 | return (buffer.getvalue(),) 32 | 33 | NODE_CLASS_MAPPINGS = { 34 | "ImageToBlobNode": ImageToBlobNode 35 | } 36 | 37 | NODE_DISPLAY_NAME_MAPPINGS = { 38 | "ImageToBlobNode": "Image to Blob Node" 39 | } -------------------------------------------------------------------------------- /key_value_node.py: -------------------------------------------------------------------------------- 1 | class KeyValueNode: 2 | def __init__(self): 3 | pass 4 | 5 | @classmethod 6 | def INPUT_TYPES(s): 7 | return { 8 | "required": { 9 | "key": ("STRING", {"default": ""}), 10 | "value": ("STRING", {"default": ""}), 11 | }, 12 | "optional": { 13 | "KEY_VALUE": ("KEY_VALUE", {"default": None}), 14 | } 15 | } 16 | 17 | RETURN_TYPES = ("KEY_VALUE",) 18 | RETURN_NAMES = ("KEY_VALUE",) 19 | 20 | FUNCTION = "create_key_value" 21 | 22 | CATEGORY = "RequestNode/KeyValue" 23 | 24 | def create_key_value(self, key="", value="", KEY_VALUE=None): 25 | output = {} 26 | 27 | if KEY_VALUE is not None: 28 | output.update(KEY_VALUE) 29 | 30 | if key and value: 31 | output[key] = value 32 | 33 | return (output,) 34 | 35 | NODE_CLASS_MAPPINGS = { 36 | "KeyValueNode": KeyValueNode 37 | } 38 | 39 | NODE_DISPLAY_NAME_MAPPINGS = { 40 | "KeyValueNode": "Key/Value Node" 41 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 felixszeto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /string_replace_node.py: -------------------------------------------------------------------------------- 1 | class StringReplaceNode: 2 | def __init__(self): 3 | pass 4 | 5 | @classmethod 6 | def INPUT_TYPES(s): 7 | return { 8 | "required": { 9 | "input_string": ("STRING", {"default": "", "multiline": True}), 10 | }, 11 | "optional": { 12 | "placeholders": ("KEY_VALUE", {"default": None}), 13 | } 14 | } 15 | 16 | RETURN_TYPES = ("STRING",) 17 | RETURN_NAMES = ("output_string",) 18 | 19 | FUNCTION = "replace_string" 20 | 21 | CATEGORY = "RequestNode/Utils" 22 | 23 | def replace_string(self, input_string, placeholders=None): 24 | if not placeholders: 25 | return (input_string,) 26 | 27 | output_string = input_string 28 | 29 | for key, value in placeholders.items(): 30 | str_value = str(value) 31 | output_string = output_string.replace(key, str_value) 32 | 33 | return (output_string,) 34 | 35 | NODE_CLASS_MAPPINGS = { 36 | "StringReplaceNode": StringReplaceNode 37 | } 38 | 39 | NODE_DISPLAY_NAME_MAPPINGS = { 40 | "StringReplaceNode": "String Replace Node" 41 | } -------------------------------------------------------------------------------- /image_to_base64_node.py: -------------------------------------------------------------------------------- 1 | import io 2 | import base64 3 | import torch 4 | import numpy as np 5 | from PIL import Image 6 | 7 | class ImageToBase64Node: 8 | def __init__(self): 9 | pass 10 | 11 | @classmethod 12 | def INPUT_TYPES(cls): 13 | return { 14 | "required": { 15 | "image": ("IMAGE",), 16 | }, 17 | } 18 | 19 | RETURN_TYPES = ("STRING",) 20 | FUNCTION = "convert_image_to_base64" 21 | CATEGORY = "RequestNode/Converters" 22 | 23 | def convert_image_to_base64(self, image): 24 | # Convert tensor to PIL Image 25 | i = 255. * image.cpu().numpy().squeeze() 26 | img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) 27 | 28 | # Save image to a byte buffer 29 | buffer = io.BytesIO() 30 | img.save(buffer, format="PNG") 31 | 32 | # Encode the byte buffer to a base64 string 33 | base64_string = base64.b64encode(buffer.getvalue()).decode('utf-8') 34 | 35 | return (base64_string,) 36 | 37 | NODE_CLASS_MAPPINGS = { 38 | "ImageToBase64Node": ImageToBase64Node 39 | } 40 | 41 | NODE_DISPLAY_NAME_MAPPINGS = { 42 | "ImageToBase64Node": "Image to Base64 Node" 43 | } -------------------------------------------------------------------------------- /retry_setting_node.py: -------------------------------------------------------------------------------- 1 | class RetrySettingNode: 2 | def __init__(self): 3 | pass 4 | 5 | @classmethod 6 | def INPUT_TYPES(s): 7 | return { 8 | "required": { 9 | "key": (["max_retry", "retry_interval", "retry_until_status_code", "retry_until_not_status_code"], {"default": ""}, {"default": "max_retry"}), 10 | "value": ("INT", {"default": 3, "min": 0, "max": 99999}), 11 | }, 12 | "optional": { 13 | "RETRY_SETTING": ("RETRY_SETTING", {"default": None}), 14 | } 15 | } 16 | 17 | RETURN_TYPES = ("RETRY_SETTING",) 18 | RETURN_NAMES = ("RETRY_SETTING",) 19 | 20 | FUNCTION = "create_retry_setting" 21 | 22 | CATEGORY = "RequestNode/KeyValue" 23 | 24 | def create_retry_setting(self, key="", value="", RETRY_SETTING=None): 25 | output = {} 26 | 27 | if RETRY_SETTING is not None: 28 | output.update(RETRY_SETTING) 29 | 30 | if key and value: 31 | output[key] = value 32 | 33 | return (output,) 34 | 35 | NODE_CLASS_MAPPINGS = { 36 | "RetrySettingNode": RetrySettingNode 37 | } 38 | 39 | NODE_DISPLAY_NAME_MAPPINGS = { 40 | "RetrySettingNode": "Retry Settings Node" 41 | } -------------------------------------------------------------------------------- /get_node.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | 4 | class GetRequestNode: 5 | def __init__(self): 6 | pass 7 | 8 | @classmethod 9 | def INPUT_TYPES(s): 10 | return { 11 | "required": { 12 | "target_url": ("STRING", {"default": "https://example.com/api"}), 13 | }, 14 | "optional": { 15 | "headers": ("KEY_VALUE", {"default": None}), 16 | "query_list": ("KEY_VALUE", {"default": []}), 17 | } 18 | } 19 | 20 | RETURN_TYPES = ("STRING", "BYTES", "JSON", "ANY") 21 | RETURN_NAMES = ("text", "file", "json", "any") 22 | 23 | FUNCTION = "make_get_request" 24 | 25 | CATEGORY = "RequestNode/Get Request" 26 | 27 | def make_get_request(self, target_url, headers=None, query_list=None): 28 | request_headers = {'Content-Type': 'application/json'} 29 | if headers: 30 | request_headers.update(headers) 31 | 32 | 33 | try: 34 | response = requests.get(target_url, params=query_list, headers=request_headers) 35 | 36 | text_output = response.text 37 | file_output = response.content 38 | 39 | try: 40 | json_output = response.json() 41 | except json.JSONDecodeError: 42 | json_output = {"error": "Response is not valid JSON"} 43 | 44 | any_output = response.content 45 | 46 | except Exception as e: 47 | error_message = str(e) 48 | text_output = error_message 49 | file_output = error_message.encode() 50 | json_output = {"error": error_message} 51 | any_output = error_message.encode() 52 | 53 | return (text_output, file_output, json_output, any_output) 54 | 55 | NODE_CLASS_MAPPINGS = { 56 | "GetRequestNode": GetRequestNode 57 | } 58 | 59 | NODE_DISPLAY_NAME_MAPPINGS = { 60 | "GetRequestNode": "Get Request Node" 61 | } 62 | -------------------------------------------------------------------------------- /form_post_node.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import io 4 | import torch 5 | import numpy as np 6 | from PIL import Image 7 | 8 | class FormPostRequestNode: 9 | def __init__(self): 10 | pass 11 | 12 | @classmethod 13 | def INPUT_TYPES(s): 14 | return { 15 | "required": { 16 | "target_url": ("STRING", {"default": "http://127.0.0.1:7788/api/echo"}), 17 | "image": ("IMAGE",), 18 | "image_field_name": ("STRING", {"default": "image"}), 19 | }, 20 | "optional": { 21 | "form_fields": ("KEY_VALUE",), 22 | "headers": ("KEY_VALUE",), 23 | } 24 | } 25 | 26 | RETURN_TYPES = ("STRING", "JSON", "ANY") 27 | RETURN_NAMES = ("text", "json", "any") 28 | 29 | FUNCTION = "make_form_post_request" 30 | 31 | CATEGORY = "RequestNode/Post Request" 32 | 33 | def make_form_post_request(self, target_url, image, image_field_name, form_fields=None, headers=None): 34 | files = [] 35 | for i, single_image in enumerate(image): 36 | # Convert tensor to PIL Image 37 | img_np = 255. * single_image.cpu().numpy() 38 | img = Image.fromarray(np.clip(img_np, 0, 255).astype(np.uint8)) 39 | 40 | # Save image to a byte buffer 41 | buffer = io.BytesIO() 42 | img.save(buffer, format='PNG') 43 | buffer.seek(0) 44 | 45 | files.append((image_field_name, (f'image_{i}.png', buffer, 'image/png'))) 46 | 47 | data = form_fields if form_fields else {} 48 | 49 | request_headers = {} 50 | if headers: 51 | request_headers.update(headers) 52 | 53 | try: 54 | response = requests.post(target_url, files=files, data=data, headers=request_headers) 55 | 56 | text_output = response.text 57 | 58 | try: 59 | json_output = response.json() 60 | except json.JSONDecodeError: 61 | json_output = {"error": "Response is not valid JSON"} 62 | 63 | any_output = response.content 64 | 65 | except Exception as e: 66 | error_message = str(e) 67 | text_output = error_message 68 | json_output = {"error": error_message} 69 | any_output = error_message.encode() 70 | 71 | return (text_output, json_output, any_output) 72 | 73 | NODE_CLASS_MAPPINGS = { 74 | "FormPostRequestNode": FormPostRequestNode 75 | } 76 | 77 | NODE_DISPLAY_NAME_MAPPINGS = { 78 | "FormPostRequestNode": "Form Post Request Node" 79 | } -------------------------------------------------------------------------------- /image_list_combiner_node.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import time 3 | import torch.nn.functional as F 4 | 5 | class ChainableUploadImage: 6 | """ 7 | A node that allows uploading an image and adding it to an image batch. 8 | It can be chained with other nodes of the same type to build a batch of images, 9 | similar to the default 'Load Image' node but with chaining capability. 10 | """ 11 | @classmethod 12 | def INPUT_TYPES(s): 13 | return { 14 | "required": { 15 | # This widget tells the frontend to show an upload button 16 | "image": ("IMAGE", {"image_upload": True}) 17 | }, 18 | "optional": { 19 | "image_batch_in": ("IMAGE",), 20 | } 21 | } 22 | 23 | RETURN_TYPES = ("IMAGE",) 24 | RETURN_NAMES = ("image_batch_out",) 25 | FUNCTION = "load_and_chain" 26 | CATEGORY = "RequestNode/Utils" 27 | 28 | def load_and_chain(self, image, image_batch_in=None): 29 | # The 'image' parameter from the upload widget is already a tensor. 30 | # No file loading or conversion is needed. 31 | 32 | # 'image' is a tensor of shape [1, H, W, C] 33 | if image_batch_in is None: 34 | # Start a new batch with the uploaded image. 35 | return (image,) 36 | else: 37 | # Get the dimensions of the incoming batch 38 | target_h = image_batch_in.shape[1] 39 | target_w = image_batch_in.shape[2] 40 | 41 | # Get the dimensions of the new image 42 | current_h = image.shape[1] 43 | current_w = image.shape[2] 44 | 45 | # Resize if dimensions don't match 46 | if current_h != target_h or current_w != target_w: 47 | # Permute from [B, H, W, C] to [B, C, H, W] for interpolation 48 | image_permuted = image.permute(0, 3, 1, 2) 49 | # Resize 50 | resized_image_permuted = F.interpolate( 51 | image_permuted, 52 | size=(target_h, target_w), 53 | mode='bilinear', 54 | align_corners=False 55 | ) 56 | # Permute back to [B, H, W, C] 57 | image = resized_image_permuted.permute(0, 2, 3, 1) 58 | 59 | # Concatenate the incoming batch with the new (potentially resized) image tensor. 60 | combined_tensor = torch.cat((image_batch_in, image), dim=0) 61 | return (combined_tensor,) 62 | 63 | @classmethod 64 | def IS_CHANGED(s, image, image_batch_in=None): 65 | # Since the image is an uploaded tensor, we can't hash a file. 66 | # We'll return the current time to ensure the node re-executes 67 | # whenever a new image is uploaded. 68 | return time.time() 69 | 70 | # For ComfyUI to discover this node 71 | NODE_CLASS_MAPPINGS = { 72 | "ChainableUploadImage": ChainableUploadImage 73 | } 74 | 75 | # A friendly name for the node in the UI 76 | NODE_DISPLAY_NAME_MAPPINGS = { 77 | "ChainableUploadImage": "Upload and Chain Image" 78 | } -------------------------------------------------------------------------------- /post_node.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import io 4 | 5 | class PostRequestNode: 6 | def __init__(self): 7 | pass 8 | 9 | @classmethod 10 | def INPUT_TYPES(s): 11 | return { 12 | "required": { 13 | "target_url": ("STRING", {"default": "https://example.com/api"}), 14 | "request_body": ("STRING", {"default": "{}", "multiline": True}), 15 | }, 16 | "optional": { 17 | "headers": ("KEY_VALUE", {"default": None}), 18 | "str0": ("STRING", {"default": ""}), 19 | "str1": ("STRING", {"default": ""}), 20 | "str2": ("STRING", {"default": ""}), 21 | "str3": ("STRING", {"default": ""}), 22 | "str4": ("STRING", {"default": ""}), 23 | "str5": ("STRING", {"default": ""}), 24 | "str6": ("STRING", {"default": ""}), 25 | "str7": ("STRING", {"default": ""}), 26 | "str8": ("STRING", {"default": ""}), 27 | "str9": ("STRING", {"default": ""}), 28 | } 29 | } 30 | 31 | RETURN_TYPES = ("STRING", "BYTES", "JSON", "ANY") 32 | RETURN_NAMES = ("text", "file", "json", "any") 33 | 34 | FUNCTION = "make_post_request" 35 | 36 | CATEGORY = "RequestNode/Post Request" 37 | 38 | def make_post_request(self, target_url, request_body, headers=None, str0="", str1="", str2="", str3="", str4="", str5="", str6="", str7="", str8="", str9=""): 39 | string_inputs = { 40 | "str0": str0, "str1": str1, "str2": str2, "str3": str3, "str4": str4, 41 | "str5": str5, "str6": str6, "str7": str7, "str8": str8, "str9": str9 42 | } 43 | 44 | for key, value in string_inputs.items(): 45 | placeholder = f"__{key}__" 46 | if value: 47 | request_body = request_body.replace(placeholder, value) 48 | 49 | # 嘗試解析 request_body 為 JSON 50 | try: 51 | body_data = json.loads(request_body) 52 | except json.JSONDecodeError: 53 | body_data = {"error": "Invalid JSON in request_body"} 54 | 55 | request_headers = {'Content-Type': 'application/json'} 56 | if headers: 57 | request_headers.update(headers) 58 | 59 | try: 60 | response = requests.post(target_url, json=body_data, headers=request_headers) 61 | 62 | # 準備四種不同格式的輸出 63 | text_output = response.text 64 | file_output = io.BytesIO(response.content) 65 | 66 | try: 67 | json_output = response.json() 68 | except json.JSONDecodeError: 69 | json_output = {"error": "Response is not valid JSON"} 70 | 71 | any_output = response.content 72 | 73 | except Exception as e: 74 | error_message = str(e) 75 | text_output = error_message 76 | file_output = io.BytesIO(error_message.encode()) 77 | json_output = {"error": error_message} 78 | any_output = error_message 79 | 80 | return (text_output, file_output, json_output, any_output) 81 | 82 | NODE_CLASS_MAPPINGS = { 83 | "PostRequestNode": PostRequestNode 84 | } 85 | 86 | NODE_DISPLAY_NAME_MAPPINGS = { 87 | "PostRequestNode": "Post Request Node" 88 | } 89 | -------------------------------------------------------------------------------- /workflows/post_node.json: -------------------------------------------------------------------------------- 1 | {"id":"1d4de083-2026-428d-bc3f-7e5480a9c916","revision":0,"last_node_id":32,"last_link_id":38,"nodes":[{"id":10,"type":"Key/Value Node","pos":[423.8839111328125,-814.2772827148438],"size":[315,82],"flags":{},"order":0,"mode":0,"inputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","shape":7,"type":"KEY_VALUE","link":null}],"outputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","type":"KEY_VALUE","links":[11,27]}],"properties":{"aux_id":"tszhang023/ComfyUI-RequestNodes","ver":"3f907689813c9b9db9fcdcad717aaa7b5a10815c","Node name for S&R":"Key/Value Node","cnr_id":"ComfyUI-RequestNodes"},"widgets_values":["Authorization","666555"]},{"id":15,"type":"Key/Value Node","pos":[755.8517456054688,-814.7266235351562],"size":[315,82],"flags":{},"order":1,"mode":0,"inputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","shape":7,"type":"KEY_VALUE","link":27}],"outputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","type":"KEY_VALUE","links":[28]}],"properties":{"aux_id":"tszhang023/ComfyUI-RequestNodes","ver":"3f907689813c9b9db9fcdcad717aaa7b5a10815c","Node name for S&R":"Key/Value Node","cnr_id":"ComfyUI-RequestNodes"},"widgets_values":["Content-Type","application/json"]},{"id":16,"type":"Key/Value Node","pos":[1135.3880615234375,-815.8942260742188],"size":[315,82],"flags":{},"order":2,"mode":0,"inputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","shape":7,"type":"KEY_VALUE","link":28}],"outputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","type":"KEY_VALUE","links":[38]}],"properties":{"aux_id":"tszhang023/ComfyUI-RequestNodes","ver":"3f907689813c9b9db9fcdcad717aaa7b5a10815c","Node name for S&R":"Key/Value Node","cnr_id":"ComfyUI-RequestNodes"},"widgets_values":["User-Agent","Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0"]},{"id":9,"type":"easy showAnything","pos":[1914.631591796875,-813.4629516601562],"size":[273.8968811035156,201.40432739257812],"flags":{},"order":4,"mode":0,"inputs":[{"label":"anything","localized_name":"输入任何","name":"anything","shape":7,"type":"*","link":37}],"outputs":[{"label":"output","localized_name":"输出","name":"output","type":"*","links":null}],"properties":{"cnr_id":"comfyui-easy-use","ver":"a844119335e51dfd59d757076407515194dd415e","Node name for S&R":"easy showAnything"},"widgets_values":["\nAccess Denied\n\n

Access Denied

\n \nYou don't have permission to access \"http://example.com/api/node\" on this server.

\nReference #18.c4643717.1745081482.a2cd30bf\n

https://errors.edgesuite.net/18.c4643717.1745081482.a2cd30bf

\n\n\n"]},{"id":4,"type":"Post Request Node","pos":[1526.701904296875,-816.213134765625],"size":[313.7032470703125,412],"flags":{},"order":3,"mode":0,"inputs":[{"label":"headers","localized_name":"headers","name":"headers","shape":7,"type":"KEY_VALUE","link":38}],"outputs":[{"label":"text","localized_name":"text","name":"text","type":"STRING","links":[37]},{"label":"file","localized_name":"file","name":"file","type":"BYTES","links":null},{"label":"json","localized_name":"json","name":"json","type":"JSON","links":null},{"label":"any","localized_name":"any","name":"any","type":"ANY","links":[]}],"properties":{"aux_id":"tszhang023/ComfyUI-RequestNodes","ver":"3f907689813c9b9db9fcdcad717aaa7b5a10815c","Node name for S&R":"Post Request Node","cnr_id":"ComfyUI-RequestNodes"},"widgets_values":["https://example.com/api/node","{\n\"id\": __str0__\n}","1234567890","","","","","","","","","",[false,true]]}],"links":[[27,10,0,15,0,"LIST"],[28,15,0,16,0,"LIST"],[37,4,0,9,0,"*"],[38,16,0,4,0,"KEY_VALUE"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.843258699973249,"offset":[-579.0961701473825,1022.5094707645925]},"ue_links":[]},"version":0.4} -------------------------------------------------------------------------------- /workflows/form_post_request_node.json: -------------------------------------------------------------------------------- 1 | {"id":"687e415c-3fd0-4186-a543-8f6f28a3bc03","revision":0,"last_node_id":59,"last_link_id":63,"nodes":[{"id":30,"type":"easy showAnything","pos":[-396.2269592285156,-1056.2960205078125],"size":[210,88],"flags":{},"order":7,"mode":0,"inputs":[{"label":"anything","localized_name":"anything","name":"anything","shape":7,"type":"*","link":55}],"outputs":[{"label":"output","localized_name":"output","name":"output","type":"*","links":null}],"properties":{"cnr_id":"comfyui-easy-use","ver":"a844119335e51dfd59d757076407515194dd415e","Node name for S&R":"easy showAnything"},"widgets_values":["{\n \"received_files_info\": {\n \"image\": [\n {\n \"content_type\": \"image/png\",\n \"filename\": \"image_0.png\",\n \"saved_path\": \"uploads\\\\image_0.png\"\n },\n {\n \"content_type\": \"image/png\",\n \"filename\": \"image_1.png\",\n \"saved_path\": \"uploads\\\\image_1.png\"\n }\n ]\n },\n \"received_form_data\": {\n \"KEY0\": \"VAL0\",\n \"KEY1\": \"VAL1\"\n }\n}\n"]},{"id":32,"type":"easy showAnything","pos":[-395.8673095703125,-758.5914916992188],"size":[210,88],"flags":{},"order":9,"mode":0,"inputs":[{"label":"anything","localized_name":"anything","name":"anything","shape":7,"type":"*","link":57}],"outputs":[{"label":"output","localized_name":"output","name":"output","type":"*","links":null}],"properties":{"cnr_id":"comfyui-easy-use","ver":"a844119335e51dfd59d757076407515194dd415e","Node name for S&R":"easy showAnything"},"widgets_values":["b'{\\n \"received_files_info\": {\\n \"image\": [\\n {\\n \"content_type\": \"image/png\",\\n \"filename\": \"image_0.png\",\\n \"saved_path\": \"uploads\\\\\\\\image_0.png\"\\n },\\n {\\n \"content_type\": \"image/png\",\\n \"filename\": \"image_1.png\",\\n \"saved_path\": \"uploads\\\\\\\\image_1.png\"\\n }\\n ]\\n },\\n \"received_form_data\": {\\n \"KEY0\": \"VAL0\",\\n \"KEY1\": \"VAL1\"\\n }\\n}\\n'"]},{"id":31,"type":"easy showAnything","pos":[-394.5364685058594,-899.7542114257812],"size":[210,88],"flags":{},"order":8,"mode":0,"inputs":[{"label":"anything","localized_name":"anything","name":"anything","shape":7,"type":"*","link":56}],"outputs":[{"label":"output","localized_name":"output","name":"output","type":"*","links":null}],"properties":{"cnr_id":"comfyui-easy-use","ver":"a844119335e51dfd59d757076407515194dd415e","Node name for S&R":"easy showAnything"},"widgets_values":["{\"received_files_info\": {\"image\": [{\"content_type\": \"image/png\", \"filename\": \"image_0.png\", \"saved_path\": \"uploads\\\\image_0.png\"}, {\"content_type\": \"image/png\", \"filename\": \"image_1.png\", \"saved_path\": \"uploads\\\\image_1.png\"}]}, \"received_form_data\": {\"KEY0\": \"VAL0\", \"KEY1\": \"VAL1\"}}"]},{"id":51,"type":"Key/Value Node","pos":[-1190.7154541015625,-779.3527221679688],"size":[315,82],"flags":{},"order":3,"mode":0,"inputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","shape":7,"type":"KEY_VALUE","link":59}],"outputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","type":"KEY_VALUE","links":[58]}],"properties":{"cnr_id":"ComfyUI-RequestNodes","ver":"cfaea2a98351a86ea0356969f50a1c1c922cff13","Node name for S&R":"Key/Value Node"},"widgets_values":["KEY0","VAL0"]},{"id":52,"type":"Key/Value Node","pos":[-1542.3631591796875,-780.2755737304688],"size":[315,82],"flags":{},"order":0,"mode":0,"inputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","shape":7,"type":"KEY_VALUE","link":null}],"outputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","type":"KEY_VALUE","links":[59]}],"properties":{"cnr_id":"ComfyUI-RequestNodes","ver":"cfaea2a98351a86ea0356969f50a1c1c922cff13","Node name for S&R":"Key/Value Node"},"widgets_values":["KEY1","VAL1"]},{"id":50,"type":"Form Post Request Node","pos":[-792.9194946289062,-917.7965087890625],"size":[315,122],"flags":{},"order":6,"mode":0,"inputs":[{"label":"image","localized_name":"image","name":"image","type":"IMAGE","link":61},{"label":"form_fields","localized_name":"form_fields","name":"form_fields","shape":7,"type":"KEY_VALUE","link":58},{"label":"headers","localized_name":"headers","name":"headers","shape":7,"type":"KEY_VALUE","link":null}],"outputs":[{"label":"text","localized_name":"text","name":"text","type":"STRING","links":[55]},{"label":"json","localized_name":"json","name":"json","type":"JSON","links":[56]},{"label":"any","localized_name":"any","name":"any","type":"ANY","links":[57]}],"properties":{"cnr_id":"ComfyUI-RequestNodes","ver":"cfaea2a98351a86ea0356969f50a1c1c922cff13","Node name for S&R":"Form Post Request Node"},"widgets_values":["http://127.0.0.1:7788/api/form_test","image"]},{"id":53,"type":"Chainable Upload Image","pos":[-1215.5858154296875,-920.3632202148438],"size":[380.4000244140625,46],"flags":{},"order":5,"mode":0,"inputs":[{"label":"image","localized_name":"image","name":"image","type":"IMAGE","link":60},{"label":"image_batch_in","localized_name":"image_batch_in","name":"image_batch_in","shape":7,"type":"IMAGE","link":62}],"outputs":[{"label":"image_batch_out","localized_name":"image_batch_out","name":"image_batch_out","type":"IMAGE","links":[61]}],"properties":{"cnr_id":"ComfyUI-RequestNodes","ver":"cfaea2a98351a86ea0356969f50a1c1c922cff13","Node name for S&R":"Chainable Upload Image"},"widgets_values":[]},{"id":54,"type":"Chainable Upload Image","pos":[-1691.7410888671875,-903.1041259765625],"size":[380.4000244140625,46],"flags":{},"order":4,"mode":0,"inputs":[{"label":"image","localized_name":"image","name":"image","type":"IMAGE","link":63},{"label":"image_batch_in","localized_name":"image_batch_in","name":"image_batch_in","shape":7,"type":"IMAGE","link":null}],"outputs":[{"label":"image_batch_out","localized_name":"image_batch_out","name":"image_batch_out","type":"IMAGE","links":[62]}],"properties":{"cnr_id":"ComfyUI-RequestNodes","ver":"cfaea2a98351a86ea0356969f50a1c1c922cff13","Node name for S&R":"Chainable Upload Image"},"widgets_values":[]},{"id":55,"type":"LoadImage","pos":[-1929.31787109375,-1330.998046875],"size":[210,326],"flags":{},"order":1,"mode":0,"inputs":[],"outputs":[{"label":"IMAGE","localized_name":"图像","name":"IMAGE","type":"IMAGE","links":[63]},{"label":"MASK","localized_name":"遮罩","name":"MASK","type":"MASK","links":null}],"properties":{"cnr_id":"comfy-core","ver":"0.3.27","Node name for S&R":"LoadImage"},"widgets_values":["kv3uu9djT8Myqz9ewkmoNcQHcuYT05.png","image"]},{"id":25,"type":"LoadImage","pos":[-1553.28271484375,-1322.3248291015625],"size":[210,326],"flags":{},"order":2,"mode":0,"inputs":[],"outputs":[{"label":"IMAGE","localized_name":"图像","name":"IMAGE","type":"IMAGE","links":[37,60]},{"label":"MASK","localized_name":"遮罩","name":"MASK","type":"MASK","links":null}],"properties":{"cnr_id":"comfy-core","ver":"0.3.27","Node name for S&R":"LoadImage"},"widgets_values":["ki66DdaYIYIKrOSP14nNquV6O81e9Z1.png","image"]}],"links":[[37,25,0,36,0,"IMAGE"],[55,50,0,30,0,"*"],[56,50,1,31,0,"*"],[57,50,2,32,0,"*"],[58,51,0,50,1,"KEY_VALUE"],[59,52,0,51,0,"KEY_VALUE"],[60,25,0,53,0,"IMAGE"],[61,53,0,50,0,"IMAGE"],[62,54,0,53,1,"IMAGE"],[63,55,0,54,0,"IMAGE"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.7400249944258218,"offset":[1977.6944567844214,1484.416712309817]}},"version":0.4} -------------------------------------------------------------------------------- /base_flask_server.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify, Response, send_file 2 | import json 3 | from flask_cors import CORS 4 | from flask_socketio import SocketIO 5 | import io 6 | import traceback 7 | from werkzeug.exceptions import HTTPException 8 | import random 9 | import os 10 | 11 | app = Flask(__name__) 12 | CORS(app) 13 | 14 | socketio = SocketIO(app, cors_allowed_origins='*') 15 | 16 | @app.before_request 17 | def log_request_info(): 18 | print(f"\n--- Received Request ---") 19 | print(f"Method: {request.method}") 20 | print(f"Path: {request.path}") 21 | print(f"Headers: {request.headers}") 22 | if request.method == 'GET': 23 | print(f"Query Parameters: {request.args}") 24 | elif request.method in ['POST', 'PUT', 'PATCH']: 25 | try: 26 | print(f"Request Body (JSON): {request.get_json(silent=True)}") 27 | except: 28 | print(f"Request Body (Form): {request.form}") 29 | else: 30 | print(f"Request Data: {request.data}") 31 | print(f"------------------------\n") 32 | 33 | @app.errorhandler(Exception) 34 | def handle_exception(e): 35 | # 打印详细的错误堆栈信息 36 | print("发生错误:", str(e)) 37 | print(traceback.format_exc()) 38 | 39 | if isinstance(e, HTTPException): 40 | response = jsonify({ 41 | "error": str(e), 42 | "status_code": e.code, 43 | "description": e.description 44 | }) 45 | response.status_code = e.code 46 | return response 47 | 48 | response = jsonify({ 49 | "error": "服务器内部错误", 50 | "details": str(e) 51 | }) 52 | response.status_code = 500 53 | return response 54 | 55 | @app.route('/api/echo', methods=['POST', 'PUT', 'PATCH']) 56 | def echo_request_body(): 57 | print(f"Received {request.method} request on /api/echo") 58 | try: 59 | data = request.get_json(silent=True) 60 | if data is None: 61 | data = request.form.to_dict() 62 | 63 | headers_info = { 64 | 'Content-Type': request.headers.get('Content-Type'), 65 | 'User-Agent': request.headers.get('User-Agent'), 66 | 'Accept': request.headers.get('Accept') 67 | } 68 | 69 | response_data = { 70 | "received_method": request.method, 71 | "received_data": data, 72 | "received_headers": headers_info 73 | } 74 | print(f"Echoing data: {response_data}") 75 | return jsonify(response_data) 76 | except Exception as e: 77 | print(f"处理 /api/echo 请求时出错: {str(e)}") 78 | return jsonify({"error": "处理请求时发生错误", "details": str(e)}), 500 79 | 80 | @app.route('/api/status/', methods=['GET']) 81 | def return_status_code(code): 82 | print(f"Received GET request on /api/status/{code}") 83 | try: 84 | if code < 100 or code > 599: 85 | print(f"错误: 无效的状态码: {code}") 86 | return jsonify({"error": f"Invalid status code: {code}"}), 400 87 | 88 | print(f"Returning response with status code: {code}") 89 | return jsonify({"message": f"Returning with status code {code}"}), code 90 | except Exception as e: 91 | print(f"处理 /api/status/{code} 请求时出错: {str(e)}") 92 | return jsonify({"error": "处理请求时发生错误", "details": str(e)}), 500 93 | 94 | 95 | @app.route('/api/headers', methods=['GET']) 96 | def return_custom_headers(): 97 | print("Received GET request on /api/headers") 98 | response = jsonify({"message": "This response has custom headers"}) 99 | response.headers['X-Custom-Header'] = 'Custom Value' 100 | response.headers['X-Another-Header'] = 'Another Value' 101 | response.headers['X-Request-Method'] = request.method # Example using request context 102 | print("Returning response with custom headers") 103 | return response 104 | 105 | @app.route('/api/file', methods=['GET']) 106 | def return_test_file(): 107 | print("Received GET request on /api/file") 108 | try: 109 | # Create a simple text file in memory and return it 110 | file_content = b"This is the content of the test file from /api/file." 111 | file_data = io.BytesIO(file_content) 112 | file_data.seek(0) 113 | print("Sending file: test_file.txt") 114 | return send_file( 115 | file_data, 116 | mimetype="text/plain", 117 | as_attachment=True, 118 | download_name="test_file.txt", 119 | headers={'X-File-Source': '/api/file'} 120 | ) 121 | except Exception as e: 122 | print(f"发送文件时出错: {str(e)}") 123 | return jsonify({"error": "发送文件时发生错误", "details": str(e)}), 500 124 | 125 | @app.route('/api/random_failure', methods=['GET']) 126 | def random_failure_endpoint(): 127 | print("Received GET request on /api/random_failure") 128 | if random.random() < 0.2: 129 | print("Simulating random 500 error") 130 | return jsonify({"error": "Simulated internal server error from /api/random_failure"}), 500 131 | else: 132 | print("Returning successful response from /api/random_failure") 133 | return jsonify({"message": "Success from /api/random_failure", "status": "ok"}) 134 | 135 | @app.route('/api/request_info', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']) 136 | def return_request_info(): 137 | print(f"Received {request.method} request on /api/request_info") 138 | try: 139 | request_info = { 140 | "method": request.method, 141 | "path": request.path, 142 | "headers": dict(request.headers), # Convert headers to dict for JSON serialization 143 | "query_parameters": request.args.to_dict(), 144 | "form_data": request.form.to_dict(), 145 | "json_data": request.get_json(silent=True), # silent=True prevents error if body is not JSON 146 | "raw_data": request.data.decode('utf-8', errors='ignore') # Attempt to decode raw data 147 | } 148 | print("Returning request info as JSON") 149 | return jsonify(request_info) 150 | except Exception as e: 151 | print(f"处理 /api/request_info 请求时出错: {str(e)}") 152 | return jsonify({"error": "处理请求时发生错误", "details": str(e)}), 500 153 | 154 | @app.route('/api/form_test', methods=['POST']) 155 | def handle_form_post(): 156 | print(f"Received POST request on /api/form_test") 157 | try: 158 | form_data = request.form.to_dict() 159 | files_info = {} 160 | if request.files: 161 | upload_folder = 'uploads' 162 | if not os.path.exists(upload_folder): 163 | os.makedirs(upload_folder) 164 | 165 | for field_name in request.files: 166 | files_info[field_name] = [] 167 | for file_storage in request.files.getlist(field_name): 168 | filename = file_storage.filename 169 | file_path = os.path.join(upload_folder, filename) 170 | file_storage.save(file_path) 171 | 172 | files_info[field_name].append({ 173 | "filename": filename, 174 | "content_type": file_storage.content_type, 175 | "saved_path": file_path 176 | }) 177 | 178 | response_data = { 179 | "received_form_data": form_data, 180 | "received_files_info": files_info 181 | } 182 | print(f"Form test data: {response_data}") 183 | return jsonify(response_data) 184 | except Exception as e: 185 | print(f"处理 /api/form_test 请求时出错: {str(e)}") 186 | return jsonify({"error": "处理请求时发生错误", "details": str(e)}), 500 187 | 188 | 189 | if __name__ == "__main__": 190 | print("Starting RestApiNode test server...") 191 | print("API available at http://127.0.0.1:7788/api/*") 192 | print("Root endpoint available at http://127.0.0.1:7788/") 193 | socketio.run(app, debug=True, host='0.0.0.0', port=7788, use_reloader=True) -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | # ComfyUI-RequestNodes 2 | 3 | [English Version](README.md) 4 | 5 | ## 介紹 6 | 7 | ComfyUI-RequestNodes 是一個用於 ComfyUI 的自訂節點插件,提供了發送 HTTP 請求及相關實用工具的功能。目前,它包含以下節點: 8 | 9 | * **Get Request Node**: 發送 GET 請求並檢索響應。 10 | * **Post Request Node**: 發送 POST 請求並檢索響應。 11 | * **Form Post Request Node**: 發送 `multipart/form-data` 格式的 POST 請求,支援檔案(圖片)上傳。 12 | * **Rest Api Node**: 一個多功能的節點,用於發送各種 HTTP 方法 (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS) 並支援重試設定。 13 | * **Image to Base64 Node**: 將圖片轉換為 Base64 編碼的字串。 14 | * **Image to Blob Node**: 將圖片轉換為 Blob (二進位大型物件)。 15 | * **Key/Value Node**: 創建鍵/值對,用於構建請求參數、標頭或其他類似字典的結構。 16 | * **Chain Image Node**: 上傳圖片並將其添加到圖片批次中,支援鏈式操作以構建多圖批次。 17 | * **String Replace Node**: 使用提供的值替換字串中的佔位符。 18 | * **Retry Settings Node**: 為 Rest Api Node 創建重試設定配置。 19 | 20 | ## 測試資源 21 | 22 | 插件包含以下測試資源: 23 | * `base_flask_server.py` - 用於測試的 Python Flask 伺服器 24 | * `get_node.json` - GET 請求工作流程模板 25 | ![rest_node](workflows/get_node.png) 26 | * `post_node.json` - POST 請求工作流程模板 27 | ![rest_node](workflows/post_node.png) 28 | * `form_post_request_node.json` - FORM POST 請求工作流程模板 29 | ![rest_node](workflows/form_post_request_node.png) 30 | * `workflows/rest_node.json` - REST API 請求工作流程模板 31 | ![rest_node](workflows/rest_node.png) 32 | 33 | ## 安裝 34 | 35 | 要安裝 ComfyUI-RequestNodes,請按照以下步驟操作: 36 | 37 | 1. **打開 ComfyUI 的 custom_nodes 目錄。** 38 | * 在您的 ComfyUI 安裝目錄中,找到 `custom_nodes` 資料夾。 39 | 40 | 2. **克隆 ComfyUI-RequestNodes 儲存庫。** 41 | * 在 `custom_nodes` 目錄中打開終端或命令提示符。 42 | * 運行以下命令克隆儲存庫: 43 | 44 | ```bash 45 | git clone https://github.com/felixszeto/ComfyUI-RequestNodes.git 46 | ``` 47 | 48 | 3. **重新啟動 ComfyUI。** 49 | * 關閉並重新啟動 ComfyUI 以載入新安裝的節點。 50 | 51 | ## 使用方法 52 | 53 | 安裝後,您可以在 ComfyUI 節點列表的 "RequestNode" 分類下找到這些節點,並有 "Get Request", "Post Request", "REST API", 和 "Utils" 等子分類。 54 | 55 | * **Get Request Node**: 56 | * **分類**: RequestNode/Get Request 57 | * **輸入**: 58 | * `target_url` (STRING, 必需): 要發送 GET 請求的 URL。 59 | * `headers` (KEY_VALUE, 可選): 請求標頭,通常來自 Key/Value Node。 60 | * `query_list` (KEY_VALUE, 可選): 查詢參數,通常來自 Key/Value Node。 61 | * **輸出**: 62 | * `text` (STRING): 響應主體作為文本。 63 | * `file` (BYTES): 響應主體作為字節。 64 | * `json` (JSON): 響應主體解析為 JSON (如果有效)。 65 | * `any` (ANY): 原始響應內容。 66 | * ![image](https://github.com/user-attachments/assets/cdb1938f-f8a9-4a4b-a787-90fa4d543523) 67 | 68 | * **Post Request Node**: 69 | * **分類**: RequestNode/Post Request 70 | * **輸入**: 71 | * `target_url` (STRING, 必需): 要發送 POST 請求的 URL。 72 | * `request_body` (STRING, 必需, 多行): 請求主體,通常為 JSON 格式。可以使用 `__str0__`, `__str1__`, ..., `__str9__` 等佔位符,它們將被對應的可選字串輸入替換。 73 | * `headers` (KEY_VALUE, 可選): 請求標頭,通常來自 Key/Value Node。 74 | * `str0` 到 `str9` (STRING, 可選): 用於替換 `request_body` 中佔位符的字串輸入。 75 | * **輸出**: 76 | * `text` (STRING): 響應主體作為文本。 77 | * `file` (BYTES): 響應主體作為字節。 78 | * `json` (JSON): 響應主體解析為 JSON (如果有效)。 79 | * `any` (ANY): 原始響應內容。 80 | * ![image](https://github.com/user-attachments/assets/6eda9fef-48cf-478c-875e-6bd6d850bff2) 81 | 82 | * **Form Post Request Node**: 83 | * **分類**: RequestNode/Post Request 84 | * **輸入**: 85 | * `target_url` (STRING, 必需): 要發送 POST 請求的 URL。 86 | * `image` (IMAGE, 必需): 要上傳的圖片或圖片批次。如果提供圖片批次,所有圖片將在同一個請求中發送。 87 | * `image_field_name` (STRING, 必需): 圖片在表單中的欄位名稱。 88 | * `form_fields` (KEY_VALUE, 可選): 其他表單欄位,通常來自 Key/Value Node。 89 | * `headers` (KEY_VALUE, 可選): 請求標頭,通常來自 Key/Value Node。 90 | * **輸出**: 91 | * `text` (STRING): 響應主體作為文本。 92 | * `json` (JSON): 響應主體解析為 JSON (如果有效)。 93 | * `any` (ANY): 原始響應內容。 94 | 95 | * **Image to Base64 Node**: 96 | * **分類**: RequestNode/Converters 97 | * **輸入**: 98 | * `image` (IMAGE, 必需): 要轉換的圖片。 99 | * **輸出**: 100 | * `STRING`: Base64 編碼的圖片字串。 101 | 102 | * **Image to Blob Node**: 103 | * **分類**: RequestNode/Converters 104 | * **輸入**: 105 | * `image` (IMAGE, 必需): 要轉換的圖片。 106 | * **輸出**: 107 | * `BYTES`: 圖片的原始二進位資料。 108 | 109 | * **Rest Api Node**: 110 | * **分類**: RequestNode/REST API 111 | * **輸入**: 112 | * `target_url` (STRING, 必需): 請求的 URL。 113 | * `method` (下拉選單, 必需): 要使用的 HTTP 方法 (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS)。 114 | * `request_body` (STRING, 必需, 多行): 請求主體 (對於 HEAD, OPTIONS, DELETE 方法會隱藏)。 115 | * `headers` (KEY_VALUE, 可選): 請求標頭,通常來自 Key/Value Node。 116 | * `RETRY_SETTING` (RETRY_SETTING, 可選): 重試設定,通常來自 Retry Settings Node。 117 | * **輸出**: 118 | * `text` (STRING): 響應主體作為文本。 119 | * `file` (BYTES): 響應主體作為字節。 120 | * `json` (JSON): 響應主體解析為 JSON (如果有效)。對於 HEAD 請求,此輸出包含響應標頭。 121 | * `headers` (DICT): 響應標頭作為字典。 122 | * `status_code` (INT): 響應的 HTTP 狀態碼。 123 | * `any` (ANY): 原始響應內容。 124 | 125 | * **Key/Value Node**: 126 | * **分類**: RequestNode/KeyValue 127 | * **輸入**: 128 | * `key` (STRING, 必需): 鍵名。 129 | * `value` (STRING, 必需): 鍵值。 130 | * `KEY_VALUE` (KEY_VALUE, 可選): 連接其他 Key/Value Node 的輸出以合併鍵值對。 131 | * **輸出**: 132 | * `KEY_VALUE` (KEY_VALUE): 包含鍵值對的字典。 133 | * ![image](https://github.com/user-attachments/assets/dfe7dab0-2b1b-4f99-ac6f-89e01d03b7e0) 134 | 135 | * **Chain Image Node**: 136 | * **分類**: RequestNode/Utils 137 | * **說明**: 此節點允許您上傳一張圖片並將其添加到圖片批次中。您可以將多個此類型的節點鏈接在一起,以創建包含多張圖片的批次。如果您上傳的圖片尺寸不同,此節點會自動將後續圖片的大小調整為與批次中第一張圖片的尺寸相符。 138 | * **輸入**: 139 | * `image` (IMAGE, 必需): 要上傳的圖片。請使用 "choose file to upload" 按鈕。 140 | * `image_batch_in` (IMAGE, 可選): 一個已有的圖片批次,用於附加新上傳的圖片。可連接另一個 `Chain Image Node` 的輸出來構建批次。 141 | * **輸出**: 142 | * `image_batch_out` (IMAGE): 合併後的圖片批次。 143 | * **範例**: 144 | * ![image](workflows/chainable_upload_image_node.png) 145 | 146 | * **String Replace Node**: 147 | * **分類**: RequestNode/Utils 148 | * **輸入**: 149 | * `input_string` (STRING, 必需, 多行): 包含要替換的佔位符的字串。 150 | * `placeholders` (KEY_VALUE, 可選): 鍵值對,其中鍵是佔位符 (例如,`__my_placeholder__`),值是替換字串。 151 | * **輸出**: 152 | * `output_string` (STRING): 替換佔位符後的字串。 153 | 154 | * **Retry Settings Node**: 155 | * **分類**: RequestNode/KeyValue 156 | * **輸入**: 157 | * `key` (下拉選單, 必需): 重試設定的鍵 (`max_retry`, `retry_interval`, `retry_until_status_code`, `retry_until_not_status_code`)。 158 | * `value` (INT, 必需): 重試設定的整數值。 159 | * `RETRY_SETTING` (RETRY_SETTING, 可選): 連接其他 Retry Settings Node 的輸出以合併設定。 160 | * **輸出**: 161 | * `RETRY_SETTING` (RETRY_SETTING): 包含重試設定的字典。 162 | 163 | **重試邏輯說明:** 164 | 165 | `Retry Settings Node` 允許您為 `Rest Api Node` 配置在滿足特定條件時自動重試。重試邏輯如下: 166 | 167 | * **`max_retry`**: 定義在初始嘗試失敗後重試請求的最大次數。例如,`max_retry: 3` 表示總共 1 (初始) + 3 (重試) = 4 次嘗試。如果設為 0 *且* 定義了特定的狀態碼條件,則會無限重試。如果未提供任何重試設定,則默認不重試。如果連接了 `RETRY_SETTING` 輸入但未明確設置 `max_retry`,則默認為 3 次重試。 168 | * **`retry_interval`**: 指定重試嘗試之間的延遲時間(毫秒)(默認為 1000 毫秒)。 169 | * **`retry_until_status_code`**: 節點將*持續*重試,*只要*收到的 HTTP 狀態碼*不*等於此值(且未達到 `max_retry` 限制)。適用於等待特定的成功代碼(例如 200)。 170 | * **`retry_until_not_status_code`**: 節點將*持續*重試,*只要*收到的 HTTP 狀態碼*等於*此值(且未達到 `max_retry` 限制)。適用於在特定的臨時錯誤代碼上重試(例如,當狀態為 202 Accepted 時重試)。 171 | * **默認重試條件 (Non-2xx):** 如果設置了 `max_retry`(或默認為 3)但*既未*指定 `retry_until_status_code` *也未*指定 `retry_until_not_status_code`,則節點將*僅在*狀態碼*不在* 200-299 範圍內(即非成功響應)時重試。 172 | * **異常情況:** 任何網絡或請求異常也會觸發重試嘗試,並遵守 `max_retry` 限制和 `retry_interval`。 173 | * **優先級:** 如果同時設置了 `retry_until_status_code` 和 `retry_until_not_status_code`,則必須滿足(或分別不滿足)這兩個條件才能根據狀態碼停止重試。僅在*未*設置特定狀態碼條件時,才會檢查默認的 non-2xx 條件。 174 | 175 | 176 | ## 貢獻 177 | 178 | 歡迎提交 issues 和 pull requests 來改進 ComfyUI-RequestNodes! 179 | 180 | --- 181 | 182 | **注意:** 183 | 184 | * 請確保您的 ComfyUI 環境已正確安裝 Git。 185 | * 如果您的 ComfyUI 安裝目錄不在默認位置,請根據您的實際情況調整路徑。 186 | * 如果您遇到任何問題,請查看 GitHub 儲存庫的 issue 頁面或提交新的 issue。 187 | -------------------------------------------------------------------------------- /rest_api_node.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import io 4 | 5 | class RestApiNode: 6 | def __init__(self): 7 | pass 8 | 9 | @classmethod 10 | def INPUT_TYPES(s): 11 | return { 12 | "required": { 13 | "target_url": ("STRING", {"default": "https://example.com/api", "forceInput":True}), 14 | "request_body": ("STRING", {"default": "{}", "multiline": True, "forceInput":True}), 15 | "method": (["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"], {"default": "GET"}), 16 | }, 17 | "optional": { 18 | "headers": ("KEY_VALUE", {"default": None}), 19 | "RETRY_SETTING": ("RETRY_SETTING", {"default": None}), 20 | } 21 | } 22 | 23 | @classmethod 24 | def HIDE_INPUTS(s, method): 25 | hide_inputs = {} 26 | 27 | if method in ["HEAD", "OPTIONS", "DELETE"]: 28 | hide_inputs["request_body"] = True 29 | 30 | return hide_inputs 31 | 32 | RETURN_TYPES = ("STRING", "BYTES", "JSON", "DICT", "INT", "ANY") 33 | RETURN_NAMES = ("text", "file", "json", "headers", "status_code", "any") 34 | 35 | FUNCTION = "make_request" 36 | 37 | CATEGORY = "RequestNode/REST API" 38 | 39 | def make_request(self, target_url, method, request_body, headers=None, RETRY_SETTING=None): 40 | 41 | request_headers = {'Content-Type': 'application/json'} 42 | if headers: 43 | request_headers.update(headers) 44 | 45 | default_max_retry = 3 46 | max_retry = default_max_retry 47 | retry_until_status_code = 0 48 | retry_until_not_status_code = 0 49 | retry_interval = 1000 50 | retry_non_2xx = False 51 | 52 | if RETRY_SETTING is not None: 53 | if "max_retry" in RETRY_SETTING: 54 | max_retry = RETRY_SETTING["max_retry"] 55 | retry_until_status_code = RETRY_SETTING.get("retry_until_status_code", 0) 56 | retry_until_not_status_code = RETRY_SETTING.get("retry_until_not_status_code", 0) 57 | retry_interval = RETRY_SETTING.get("retry_interval", 1000) 58 | 59 | if "max_retry" in RETRY_SETTING and not any(key in RETRY_SETTING for key in ["retry_until_status_code", "retry_until_not_status_code"]): 60 | retry_non_2xx = True 61 | 62 | params = {} 63 | body_data = None 64 | 65 | if method in ["POST", "PUT", "PATCH"]: 66 | try: 67 | body_data = json.loads(request_body) 68 | except json.JSONDecodeError: 69 | body_data = {"error": "Invalid JSON in request_body"} 70 | elif method == "GET": 71 | try: 72 | params_data = json.loads(request_body) 73 | if isinstance(params_data, dict): 74 | params = params_data 75 | except json.JSONDecodeError: 76 | pass 77 | 78 | def send_request(): 79 | if method == "GET": 80 | return requests.get(target_url, params=params, headers=request_headers) 81 | elif method == "POST": 82 | return requests.post(target_url, json=body_data, headers=request_headers) 83 | elif method == "PUT": 84 | return requests.put(target_url, json=body_data, headers=request_headers) 85 | elif method == "DELETE": 86 | return requests.delete(target_url, headers=request_headers) 87 | elif method == "PATCH": 88 | return requests.patch(target_url, json=body_data, headers=request_headers) 89 | elif method == "HEAD": 90 | return requests.head(target_url, headers=request_headers) 91 | elif method == "OPTIONS": 92 | return requests.options(target_url, headers=request_headers) 93 | else: 94 | raise ValueError(f"Unsupported HTTP method: {method}") 95 | 96 | max_attempts = 1 97 | attempts = 0 98 | last_exception = None 99 | 100 | has_specific_retry_conditions = (retry_until_status_code > 0 or retry_until_not_status_code > 0 or retry_non_2xx) 101 | 102 | if has_specific_retry_conditions and max_retry == 0: 103 | import sys 104 | max_attempts = sys.maxsize 105 | elif max_retry > 0: 106 | max_attempts = max_retry + 1 107 | else: 108 | max_attempts = 1 109 | 110 | try: 111 | while attempts < max_attempts: 112 | try: 113 | attempts += 1 114 | response = send_request() 115 | 116 | if retry_until_status_code > 0 and response.status_code != retry_until_status_code: 117 | if attempts < max_attempts: 118 | import time 119 | time.sleep(retry_interval / 1000) 120 | continue 121 | elif retry_until_not_status_code > 0 and response.status_code == retry_until_not_status_code: 122 | if attempts < max_attempts: 123 | import time 124 | time.sleep(retry_interval / 1000) 125 | continue 126 | elif retry_non_2xx and (response.status_code < 200 or response.status_code >= 300): 127 | if attempts < max_attempts: 128 | import time 129 | time.sleep(retry_interval / 1000) 130 | continue 131 | 132 | break 133 | 134 | except Exception as e: 135 | last_exception = e 136 | if attempts < max_attempts: 137 | import time 138 | time.sleep(retry_interval / 1000) 139 | else: 140 | raise 141 | 142 | text_output = response.text 143 | 144 | if method == "HEAD": 145 | file_output = io.BytesIO(b"") 146 | any_output = b"" 147 | else: 148 | file_output = io.BytesIO(response.content) 149 | any_output = response.content 150 | 151 | try: 152 | if method == "HEAD": 153 | json_output = dict(response.headers) 154 | else: 155 | json_output = response.json() 156 | except json.JSONDecodeError: 157 | if method == "HEAD": 158 | json_output = dict(response.headers) 159 | else: 160 | json_output = {"error": "Response is not valid JSON"} 161 | 162 | if attempts > 1: 163 | retry_info = {"retries": attempts - 1} 164 | if max_retry > 0: 165 | retry_info["max_retry"] = max_retry 166 | if retry_until_status_code > 0: 167 | retry_info["retry_until_status_code"] = retry_until_status_code 168 | if retry_until_not_status_code > 0: 169 | retry_info["retry_until_not_status_code"] = retry_until_not_status_code 170 | if retry_non_2xx: 171 | retry_info["retry_non_2xx"] = True 172 | if isinstance(json_output, dict): 173 | json_output["retry_info"] = retry_info 174 | 175 | except Exception as e: 176 | error_message = str(e) 177 | text_output = error_message 178 | file_output = io.BytesIO(error_message.encode()) 179 | json_output = {"error": error_message} 180 | headers_output = {"error": error_message} 181 | status_code = 0 182 | any_output = error_message 183 | else: 184 | headers_output = dict(response.headers) 185 | status_code = response.status_code 186 | 187 | return (text_output, file_output, json_output, headers_output, status_code, any_output) 188 | 189 | NODE_CLASS_MAPPINGS = { 190 | "RestApiNode": RestApiNode 191 | } 192 | 193 | NODE_DISPLAY_NAME_MAPPINGS = { 194 | "RestApiNode": "Rest Api Node" 195 | } -------------------------------------------------------------------------------- /workflows/rest_node.json: -------------------------------------------------------------------------------- 1 | {"id":"49da87f2-ba9f-4d22-975e-2d14308c2d01","revision":0,"last_node_id":29,"last_link_id":23,"nodes":[{"id":19,"type":"Retry Settings Node","pos":[168.73239135742188,450.2418518066406],"size":[342.5999755859375,82],"flags":{},"order":4,"mode":0,"inputs":[{"label":"RETRY_SETTING","localized_name":"RETRY_SETTING","name":"RETRY_SETTING","shape":7,"type":"RETRY_SETTING","link":11}],"outputs":[{"label":"RETRY_SETTING","localized_name":"RETRY_SETTING","name":"RETRY_SETTING","type":"RETRY_SETTING","links":[10]}],"properties":{"aux_id":"tszhang023/ComfyUI-RequestNodes","ver":"4fbf09ecea277fb903ec0a1cb33e46f85a5c57ee","Node name for S&R":"Retry Settings Node"},"widgets_values":["retry_interval",1000]},{"id":18,"type":"Retry Settings Node","pos":[171.41567993164062,590.2352294921875],"size":[342.5999755859375,82],"flags":{},"order":0,"mode":0,"inputs":[{"label":"RETRY_SETTING","localized_name":"RETRY_SETTING","name":"RETRY_SETTING","shape":7,"type":"RETRY_SETTING","link":null}],"outputs":[{"label":"RETRY_SETTING","localized_name":"RETRY_SETTING","name":"RETRY_SETTING","type":"RETRY_SETTING","links":[11]}],"properties":{"aux_id":"tszhang023/ComfyUI-RequestNodes","ver":"4fbf09ecea277fb903ec0a1cb33e46f85a5c57ee","Node name for S&R":"Retry Settings Node"},"widgets_values":["retry_until_not_status_code",200]},{"id":23,"type":"Key/Value Node","pos":[-216.3824920654297,1090.4547119140625],"size":[315,82],"flags":{},"order":1,"mode":0,"inputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","shape":7,"type":"KEY_VALUE","link":null}],"outputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","type":"KEY_VALUE","links":[16]}],"properties":{"aux_id":"tszhang023/ComfyUI-RequestNodes","ver":"4fbf09ecea277fb903ec0a1cb33e46f85a5c57ee","Node name for S&R":"Key/Value Node"},"widgets_values":["_itemDesc_","test update desc"]},{"id":13,"type":"ShowText|pysssss","pos":[1233.2806396484375,491.05523681640625],"size":[496.54840087890625,244.94590759277344],"flags":{},"order":11,"mode":0,"inputs":[{"label":"text","name":"text","type":"STRING","widget":{"name":"text"},"link":4}],"outputs":[{"label":"STRING","localized_name":"字符串","name":"STRING","shape":6,"type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-custom-scripts","ver":"9f7b3215e6af317603056a9a1666bf6e83e28835","Node name for S&R":"ShowText|pysssss"},"widgets_values":["","\nAccess Denied\n\n

Access Denied

\n \nYou don't have permission to access \"http://example.com/item/15\" on this server.

\nReference #18.ce20c817.1745004974.5d23d999\n

https://errors.edgesuite.net/18.ce20c817.1745004974.5d23d999

\n\n\n"]},{"id":11,"type":"Rest Api Node","pos":[724.9014892578125,481.0257568359375],"size":[400,236],"flags":{},"order":10,"mode":0,"inputs":[{"label":"headers","localized_name":"headers","name":"headers","shape":7,"type":"KEY_VALUE","link":17},{"label":"RETRY_SETTING","localized_name":"RETRY_SETTING","name":"RETRY_SETTING","shape":7,"type":"RETRY_SETTING","link":3},{"label":"target_url","name":"target_url","type":"STRING","widget":{"name":"target_url"},"link":21},{"label":"request_body","name":"request_body","type":"STRING","widget":{"name":"request_body"},"link":22}],"outputs":[{"label":"text","localized_name":"text","name":"text","type":"STRING","links":[4]},{"label":"file","localized_name":"file","name":"file","type":"BYTES","links":null},{"label":"json","localized_name":"json","name":"json","type":"JSON","links":null},{"label":"headers","localized_name":"headers","name":"headers","type":"DICT","links":null},{"label":"status_code","localized_name":"status_code","name":"status_code","type":"INT","links":[18]},{"label":"any","localized_name":"any","name":"any","type":"ANY","links":null}],"properties":{"aux_id":"tszhang023/ComfyUI-RequestNodes","ver":"4fbf09ecea277fb903ec0a1cb33e46f85a5c57ee","Node name for S&R":"Rest Api Node"},"widgets_values":["https://example.com/api","{}","POST",[false,true]]},{"id":22,"type":"Key/Value Node","pos":[-218.03587341308594,947.4794311523438],"size":[315,82],"flags":{},"order":5,"mode":0,"inputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","shape":7,"type":"KEY_VALUE","link":16}],"outputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","type":"KEY_VALUE","links":[20]}],"properties":{"aux_id":"tszhang023/ComfyUI-RequestNodes","ver":"4fbf09ecea277fb903ec0a1cb33e46f85a5c57ee","Node name for S&R":"Key/Value Node"},"widgets_values":["_itemName_","item15"]},{"id":20,"type":"Key/Value Node","pos":[-220.75303649902344,784.342041015625],"size":[315,82],"flags":{},"order":2,"mode":0,"inputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","shape":7,"type":"KEY_VALUE","link":null}],"outputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","type":"KEY_VALUE","links":[19]}],"properties":{"aux_id":"tszhang023/ComfyUI-RequestNodes","ver":"4fbf09ecea277fb903ec0a1cb33e46f85a5c57ee","Node name for S&R":"Key/Value Node"},"widgets_values":["_itemId_","15"]},{"id":28,"type":"String Replace Node","pos":[175.7892608642578,782.6775512695312],"size":[374.4447937011719,93],"flags":{},"order":6,"mode":0,"inputs":[{"label":"placeholders","localized_name":"placeholders","name":"placeholders","shape":7,"type":"KEY_VALUE","link":19}],"outputs":[{"label":"output_string","localized_name":"output_string","name":"output_string","type":"STRING","links":[21]}],"properties":{"aux_id":"tszhang023/ComfyUI-RequestNodes","ver":"4fbf09ecea277fb903ec0a1cb33e46f85a5c57ee","Node name for S&R":"String Replace Node"},"widgets_values":["https://example.com/api/item\n",[false,true]]},{"id":27,"type":"String Replace Node","pos":[175.1720733642578,945.8580932617188],"size":[400,200],"flags":{},"order":9,"mode":0,"inputs":[{"label":"placeholders","localized_name":"placeholders","name":"placeholders","shape":7,"type":"KEY_VALUE","link":20}],"outputs":[{"label":"output_string","localized_name":"output_string","name":"output_string","type":"STRING","links":[22]}],"properties":{"aux_id":"tszhang023/ComfyUI-RequestNodes","ver":"4fbf09ecea277fb903ec0a1cb33e46f85a5c57ee","Node name for S&R":"String Replace Node"},"widgets_values":["{\n\"name\": \"_itemName_\",\n\"description\": \"_itemDesc_\",\n}",[false,true]]},{"id":24,"type":"Key/Value Node","pos":[164.5167694091797,108.09188842773438],"size":[315,82],"flags":{},"order":7,"mode":0,"inputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","shape":7,"type":"KEY_VALUE","link":23}],"outputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","type":"KEY_VALUE","links":[17]}],"properties":{"aux_id":"tszhang023/ComfyUI-RequestNodes","ver":"4fbf09ecea277fb903ec0a1cb33e46f85a5c57ee","Node name for S&R":"Key/Value Node"},"widgets_values":["content-type","application/json"]},{"id":29,"type":"Key/Value Node","pos":[-208.86370849609375,109.01553344726562],"size":[315,82],"flags":{},"order":3,"mode":0,"inputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","shape":7,"type":"KEY_VALUE","link":null}],"outputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","type":"KEY_VALUE","links":[23]}],"properties":{"aux_id":"tszhang023/ComfyUI-RequestNodes","ver":"4fbf09ecea277fb903ec0a1cb33e46f85a5c57ee","Node name for S&R":"Key/Value Node"},"widgets_values":["Authorization"," Bearer xxxxxxx"]},{"id":12,"type":"Retry Settings Node","pos":[162.3536376953125,317.35546875],"size":[342.5999755859375,82],"flags":{},"order":8,"mode":0,"inputs":[{"label":"RETRY_SETTING","localized_name":"RETRY_SETTING","name":"RETRY_SETTING","shape":7,"type":"RETRY_SETTING","link":10}],"outputs":[{"label":"RETRY_SETTING","localized_name":"RETRY_SETTING","name":"RETRY_SETTING","type":"RETRY_SETTING","links":[3]}],"properties":{"aux_id":"tszhang023/ComfyUI-RequestNodes","ver":"4fbf09ecea277fb903ec0a1cb33e46f85a5c57ee","Node name for S&R":"Retry Settings Node"},"widgets_values":["max_retry",3]},{"id":26,"type":"easy showAnything","pos":[1242.66259765625,802.2389526367188],"size":[210,88],"flags":{},"order":12,"mode":0,"inputs":[{"label":"anything","localized_name":"输入任何","name":"anything","shape":7,"type":"*","link":18}],"outputs":[{"label":"output","localized_name":"输出","name":"output","type":"*","links":null}],"properties":{"cnr_id":"comfyui-easy-use","ver":"69aac075e8ba2c11d4dd3531a03eb6c7103fd1d8","Node name for S&R":"easy showAnything"},"widgets_values":["403"]}],"links":[[3,12,0,11,1,"RETRY_SETTING"],[4,11,0,13,0,"STRING"],[10,19,0,12,0,"RETRY_SETTING"],[11,18,0,19,0,"RETRY_SETTING"],[16,23,0,22,0,"KEY_VALUE"],[17,24,0,11,0,"KEY_VALUE"],[18,11,4,26,0,"*"],[19,20,0,28,0,"KEY_VALUE"],[20,22,0,27,0,"KEY_VALUE"],[21,28,0,11,2,"STRING"],[22,27,0,11,3,"STRING"],[23,29,0,24,0,"KEY_VALUE"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.6830134553650705,"offset":[2329.0169194795444,680.9430528857317]},"ue_links":[]},"version":0.4} -------------------------------------------------------------------------------- /workflows/get_node.json: -------------------------------------------------------------------------------- 1 | {"id":"1d4de083-2026-428d-bc3f-7e5480a9c916","revision":0,"last_node_id":37,"last_link_id":46,"nodes":[{"id":11,"type":"Key/Value Node","pos":[37.63900375366211,-662.89306640625],"size":[315,82],"flags":{},"order":0,"mode":0,"inputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","shape":7,"type":"KEY_VALUE","link":null}],"outputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","type":"KEY_VALUE","links":[26]}],"properties":{"aux_id":"tszhang023/ComfyUI-RequestNodes","ver":"3f907689813c9b9db9fcdcad717aaa7b5a10815c","Node name for S&R":"Key/Value Node","cnr_id":"ComfyUI-RequestNodes"},"widgets_values":["id","1235546"]},{"id":12,"type":"Key/Value Node","pos":[381.6382141113281,-663.740966796875],"size":[315,82],"flags":{},"order":2,"mode":0,"inputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","shape":7,"type":"KEY_VALUE","link":26}],"outputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","type":"KEY_VALUE","links":[20]}],"properties":{"aux_id":"tszhang023/ComfyUI-RequestNodes","ver":"3f907689813c9b9db9fcdcad717aaa7b5a10815c","Node name for S&R":"Key/Value Node","cnr_id":"ComfyUI-RequestNodes"},"widgets_values":["custName","Chan Tai Man"]},{"id":13,"type":"Key/Value Node","pos":[734.2916259765625,-665.6480712890625],"size":[315,82],"flags":{},"order":4,"mode":0,"inputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","shape":7,"type":"KEY_VALUE","link":20}],"outputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","type":"KEY_VALUE","links":[21]}],"properties":{"aux_id":"tszhang023/ComfyUI-RequestNodes","ver":"3f907689813c9b9db9fcdcad717aaa7b5a10815c","Node name for S&R":"Key/Value Node","cnr_id":"ComfyUI-RequestNodes"},"widgets_values":["dob","19870201"]},{"id":10,"type":"Key/Value Node","pos":[29.31063461303711,-814.2772216796875],"size":[315,82],"flags":{},"order":1,"mode":0,"inputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","shape":7,"type":"KEY_VALUE","link":null}],"outputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","type":"KEY_VALUE","links":[11,27]}],"properties":{"aux_id":"tszhang023/ComfyUI-RequestNodes","ver":"3f907689813c9b9db9fcdcad717aaa7b5a10815c","Node name for S&R":"Key/Value Node","cnr_id":"ComfyUI-RequestNodes"},"widgets_values":["Authorization","666555"]},{"id":15,"type":"Key/Value Node","pos":[381.15966796875,-813.990234375],"size":[315,82],"flags":{},"order":3,"mode":0,"inputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","shape":7,"type":"KEY_VALUE","link":27}],"outputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","type":"KEY_VALUE","links":[28]}],"properties":{"aux_id":"tszhang023/ComfyUI-RequestNodes","ver":"3f907689813c9b9db9fcdcad717aaa7b5a10815c","Node name for S&R":"Key/Value Node","cnr_id":"ComfyUI-RequestNodes"},"widgets_values":["Content-Type","application/json"]},{"id":9,"type":"easy showAnything","pos":[1959.76611328125,-763.2276000976562],"size":[210,180.7139129638672],"flags":{},"order":8,"mode":0,"inputs":[{"label":"anything","localized_name":"输入任何","name":"anything","shape":7,"type":"*","link":45}],"outputs":[{"label":"output","localized_name":"输出","name":"output","type":"*","links":null}],"properties":{"cnr_id":"comfyui-easy-use","ver":"a844119335e51dfd59d757076407515194dd415e","Node name for S&R":"easy showAnything"},"widgets_values":["\n\n\n Example Domain\n\n \n \n \n \n\n\n\n
\n

Example Domain

\n

This domain is for use in illustrative examples in documents. You may use this\n domain in literature without prior coordination or asking for permission.

\n

More information...

\n
\n\n\n"]},{"id":16,"type":"Key/Value Node","pos":[742.2875366210938,-816.6304931640625],"size":[315,82],"flags":{},"order":5,"mode":0,"inputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","shape":7,"type":"KEY_VALUE","link":28}],"outputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","type":"KEY_VALUE","links":[43]}],"properties":{"aux_id":"tszhang023/ComfyUI-RequestNodes","ver":"3f907689813c9b9db9fcdcad717aaa7b5a10815c","Node name for S&R":"Key/Value Node","cnr_id":"ComfyUI-RequestNodes"},"widgets_values":["User-Agent","Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0"]},{"id":33,"type":"easy showAnything","pos":[1967.5164794921875,-521.6560668945312],"size":[210,88],"flags":{},"order":9,"mode":0,"inputs":[{"label":"anything","localized_name":"输入任何","name":"anything","shape":7,"type":"*","link":46}],"outputs":[{"label":"output","localized_name":"输出","name":"output","type":"*","links":[]}],"properties":{"cnr_id":"comfyui-easy-use","ver":"69aac075e8ba2c11d4dd3531a03eb6c7103fd1d8","Node name for S&R":"easy showAnything"},"widgets_values":["b'\\n\\n\\n Example Domain\\n\\n \\n \\n \\n \\n\\n\\n\\n
\\n

Example Domain

\\n

This domain is for use in illustrative examples in documents. You may use this\\n domain in literature without prior coordination or asking for permission.

\\n

More information...

\\n
\\n\\n\\n'"]},{"id":37,"type":"Get Request Node","pos":[1545.2593994140625,-752.983642578125],"size":[315,118],"flags":{},"order":7,"mode":0,"inputs":[{"label":"headers","localized_name":"headers","name":"headers","shape":7,"type":"KEY_VALUE","link":43},{"label":"placeholders","localized_name":"placeholders","name":"placeholders","shape":7,"type":"KEY_VALUE","link":44}],"outputs":[{"label":"text","localized_name":"text","name":"text","type":"STRING","links":[45]},{"label":"file","localized_name":"file","name":"file","type":"BYTES","links":[46]},{"label":"json","localized_name":"json","name":"json","type":"JSON","links":null},{"label":"any","localized_name":"any","name":"any","type":"ANY","links":null}],"properties":{"aux_id":"tszhang023/ComfyUI-RequestNodes","ver":"4fbf09ecea277fb903ec0a1cb33e46f85a5c57ee","Node name for S&R":"Get Request Node"},"widgets_values":["https://example.com/api"]},{"id":14,"type":"Key/Value Node","pos":[1121.7100830078125,-671.9554443359375],"size":[315,82],"flags":{},"order":6,"mode":0,"inputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","shape":7,"type":"KEY_VALUE","link":21}],"outputs":[{"label":"KEY_VALUE","localized_name":"KEY_VALUE","name":"KEY_VALUE","type":"KEY_VALUE","links":[44]}],"properties":{"aux_id":"tszhang023/ComfyUI-RequestNodes","ver":"3f907689813c9b9db9fcdcad717aaa7b5a10815c","Node name for S&R":"Key/Value Node","cnr_id":"ComfyUI-RequestNodes"},"widgets_values":["address","United States"]}],"links":[[20,12,0,13,0,"DICT"],[21,13,0,14,0,"DICT"],[26,11,0,12,0,"LIST"],[27,10,0,15,0,"LIST"],[28,15,0,16,0,"LIST"],[43,16,0,37,0,"KEY_VALUE"],[44,14,0,37,1,"KEY_VALUE"],[45,37,0,9,0,"*"],[46,37,1,33,0,"*"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.7513148009015777,"offset":[-124.3523150650758,1255.3399282528917]},"ue_links":[]},"version":0.4} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ComfyUI-RequestNodes 2 | 3 | [中文版本](README_zh.md) 4 | 5 | ## Introduction 6 | 7 | ComfyUI-RequestNodes is a custom node plugin for ComfyUI that provides functionality for sending HTTP requests and related utilities. Currently, it includes the following nodes: 8 | 9 | * **Get Request Node**: Sends GET requests and retrieves responses. 10 | * **Post Request Node**: Sends POST requests and retrieves responses. 11 | * **Form Post Request Node**: Sends POST requests in `multipart/form-data` format, supporting file (image) uploads. 12 | * **Rest Api Node**: A versatile node for sending various HTTP methods (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS) with retry settings. 13 | * **Image to Base64 Node**: Converts an image to a Base64 encoded string. 14 | * **Image to Blob Node**: Converts an image to a Blob (Binary Large Object). 15 | * **Key/Value Node**: Creates key/value pairs for building request parameters, headers, or other dictionary-like structures. 16 | * **Chain Image Node**: Uploads an image and adds it to an image batch, allowing for chaining to build a batch from multiple images. 17 | * **String Replace Node**: Replaces placeholders in a string with provided values. 18 | * **Retry Settings Node**: Creates retry setting configurations for the Rest Api Node. 19 | 20 | ## Test Resources 21 | 22 | The plugin includes the following test resources: 23 | * `base_flask_server.py` - Python Flask server for testing 24 | * `get_node.json` - GET request workflow template 25 | ![rest_node](workflows/get_node.png) 26 | * `post_node.json` - POST request workflow template 27 | ![rest_node](workflows/post_node.png) 28 | * `form_post_request_node.json` - FORM POST request workflow template 29 | ![rest_node](workflows/form_post_request_node.png) 30 | * `workflows/rest_node.json` - REST API request workflow template 31 | ![rest_node](workflows/rest_node.png) 32 | 33 | ## Installation 34 | 35 | To install ComfyUI-RequestNodes, follow these steps: 36 | 37 | 1. **Open the ComfyUI custom_nodes directory.** 38 | * In your ComfyUI installation directory, find the `custom_nodes` folder. 39 | 40 | 2. **Clone the ComfyUI-RequestNodes repository.** 41 | * Open a terminal or command prompt in the `custom_nodes` directory. 42 | * Run the following command to clone the repository: 43 | 44 | ```bash 45 | git clone https://github.com/felixszeto/ComfyUI-RequestNodes.git 46 | ``` 47 | 48 | 3. **Restart ComfyUI.** 49 | * Close and restart ComfyUI to load the newly installed nodes. 50 | 51 | ## Usage 52 | 53 | After installation, you can find the nodes under the "RequestNode" category in the ComfyUI node list, with subcategories like "Get Request", "Post Request", "REST API", and "Utils". 54 | 55 | * **Get Request Node**: 56 | * **Category**: RequestNode/Get Request 57 | * **Inputs**: 58 | * `target_url` (STRING, required): The URL to send the GET request to. 59 | * `headers` (KEY_VALUE, optional): Request headers, typically from a Key/Value Node. 60 | * `query_list` (KEY_VALUE, optional): Query parameters, typically from a Key/Value Node. 61 | * **Outputs**: 62 | * `text` (STRING): The response body as text. 63 | * `file` (BYTES): The response body as bytes. 64 | * `json` (JSON): The response body parsed as JSON (if valid). 65 | * `any` (ANY): The raw response content. 66 | * ![image](https://github.com/user-attachments/assets/cdb1938f-f8a9-4a4b-a787-90fa4d543523) 67 | 68 | * **Post Request Node**: 69 | * **Category**: RequestNode/Post Request 70 | * **Inputs**: 71 | * `target_url` (STRING, required): The URL to send the POST request to. 72 | * `request_body` (STRING, required, multiline): The request body, typically in JSON format. Placeholders like `__str0__`, `__str1__`, ..., `__str9__` can be used and will be replaced by the corresponding optional string inputs. 73 | * `headers` (KEY_VALUE, optional): Request headers, typically from a Key/Value Node. 74 | * `str0` to `str9` (STRING, optional): String inputs to replace placeholders in `request_body`. 75 | * **Outputs**: 76 | * `text` (STRING): The response body as text. 77 | * `file` (BYTES): The response body as bytes. 78 | * `json` (JSON): The response body parsed as JSON (if valid). 79 | * `any` (ANY): The raw response content. 80 | * ![image](https://github.com/user-attachments/assets/6eda9fef-48cf-478c-875e-6bd6d850bff2) 81 | 82 | * **Form Post Request Node**: 83 | * **Category**: RequestNode/Post Request 84 | * **Inputs**: 85 | * `target_url` (STRING, required): The URL to send the POST request to. 86 | * `image` (IMAGE, required): The image or image batch to upload. If an image batch is provided, all images will be sent in the same request. 87 | * `image_field_name` (STRING, required): The field name for the image in the form. 88 | * `form_fields` (KEY_VALUE, optional): Other form fields, typically from a Key/Value Node. 89 | * `headers` (KEY_VALUE, optional): Request headers, typically from a Key/Value Node. 90 | * **Outputs**: 91 | * `text` (STRING): The response body as text. 92 | * `json` (JSON): The response body parsed as JSON (if valid). 93 | * `any` (ANY): The raw response content. 94 | 95 | * **Image to Base64 Node**: 96 | * **Category**: RequestNode/Converters 97 | * **Inputs**: 98 | * `image` (IMAGE, required): The image to convert. 99 | * **Outputs**: 100 | * `STRING`: The Base64 encoded image string. 101 | 102 | * **Image to Blob Node**: 103 | * **Category**: RequestNode/Converters 104 | * **Inputs**: 105 | * `image` (IMAGE, required): The image to convert. 106 | * **Outputs**: 107 | * `BYTES`: The raw binary data of the image. 108 | 109 | * **Rest Api Node**: 110 | * **Category**: RequestNode/REST API 111 | * **Inputs**: 112 | * `target_url` (STRING, required): The URL for the request. 113 | * `method` (Dropdown, required): The HTTP method to use (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS). 114 | * `request_body` (STRING, required, multiline): The request body (hidden for HEAD, OPTIONS, DELETE methods). 115 | * `headers` (KEY_VALUE, optional): Request headers, typically from a Key/Value Node. 116 | * `RETRY_SETTING` (RETRY_SETTING, optional): Retry settings, typically from a Retry Settings Node. 117 | * **Outputs**: 118 | * `text` (STRING): The response body as text. 119 | * `file` (BYTES): The response body as bytes. 120 | * `json` (JSON): The response body parsed as JSON (if valid). For HEAD requests, this output contains the response headers. 121 | * `headers` (DICT): The response headers as a dictionary. 122 | * `status_code` (INT): The HTTP status code of the response. 123 | * `any` (ANY): The raw response content. 124 | 125 | * **Key/Value Node**: 126 | * **Category**: RequestNode/KeyValue 127 | * **Inputs**: 128 | * `key` (STRING, required): The key name. 129 | * `value` (STRING, required): The key value. 130 | * `KEY_VALUE` (KEY_VALUE, optional): Connect output from other Key/Value Nodes to merge pairs. 131 | * **Outputs**: 132 | * `KEY_VALUE` (KEY_VALUE): A dictionary containing the key/value pair(s). 133 | * ![image](https://github.com/user-attachments/assets/dfe7dab0-2b1b-4f99-ac6f-89e01d03b7e0) 134 | 135 | * **Chain Image Node**: 136 | * **Category**: RequestNode/Utils 137 | * **Description**: This node allows you to upload an image and add it to an image batch. You can chain multiple nodes of this type together to create a batch of several images. If you upload images with different dimensions, this node will automatically resize subsequent images to match the dimensions of the first one in the batch. 138 | * **Inputs**: 139 | * `image` (IMAGE, required): The image to upload. Use the "choose file to upload" button. 140 | * `image_batch_in` (IMAGE, optional): An existing image batch to append the newly uploaded image to. This can be connected from another `Chain Image Node` to build a batch. 141 | * **Outputs**: 142 | * `image_batch_out` (IMAGE): The combined image batch. 143 | * **Example**: 144 | * ![image](workflows/chainable_upload_image_node.png) 145 | 146 | * **String Replace Node**: 147 | * **Category**: RequestNode/Utils 148 | * **Inputs**: 149 | * `input_string` (STRING, required, multiline): The string containing placeholders to be replaced. 150 | * `placeholders` (KEY_VALUE, optional): Key/Value pairs where keys are placeholders (e.g., `__my_placeholder__`) and values are the replacement strings. 151 | * **Outputs**: 152 | * `output_string` (STRING): The string with placeholders replaced. 153 | 154 | * **Retry Settings Node**: 155 | * **Category**: RequestNode/KeyValue 156 | * **Inputs**: 157 | * `key` (Dropdown, required): The retry setting key (`max_retry`, `retry_interval`, `retry_until_status_code`, `retry_until_not_status_code`). 158 | * `value` (INT, required): The integer value for the retry setting. 159 | * `RETRY_SETTING` (RETRY_SETTING, optional): Connect output from other Retry Settings Nodes to merge settings. 160 | * **Outputs**: 161 | * `RETRY_SETTING` (RETRY_SETTING): A dictionary containing the retry setting(s). 162 | 163 | **Retry Logic Explanation:** 164 | 165 | The `Retry Settings Node` allows you to configure automatic retries for the `Rest Api Node` when specific conditions are met. The retry logic works as follows: 166 | 167 | * **`max_retry`**: Defines the maximum number of times to retry the request after the initial attempt fails. For example, `max_retry: 3` means a total of 1 (initial) + 3 (retries) = 4 attempts. If set to 0 *and* specific status code conditions are defined, it will retry indefinitely. If no retry settings are provided, the default is no retries. If the `RETRY_SETTING` input is connected but `max_retry` is not explicitly set, it defaults to 3 retries. 168 | * **`retry_interval`**: Specifies the delay in milliseconds between retry attempts (default is 1000ms). 169 | * **`retry_until_status_code`**: The node will keep retrying *as long as* the HTTP status code received is *not* equal to this value (and `max_retry` limit is not reached). Useful for waiting for a specific success code (e.g., 200). 170 | * **`retry_until_not_status_code`**: The node will keep retrying *as long as* the HTTP status code received *is* equal to this value (and `max_retry` limit is not reached). Useful for retrying on specific temporary error codes (e.g., retrying while status is 202 Accepted). 171 | * **Default Retry Condition (Non-2xx):** If `max_retry` is set (or defaults to 3) but *neither* `retry_until_status_code` nor `retry_until_not_status_code` are specified, the node will retry *only if* the status code is *not* in the 200-299 range (i.e., a non-successful response). 172 | * **Exceptions:** Any network or request exception will also trigger a retry attempt, respecting the `max_retry` limit and `retry_interval`. 173 | * **Priority:** If both `retry_until_status_code` and `retry_until_not_status_code` are set, both conditions must be met (or not met, respectively) to stop retrying based on status code. The default non-2xx condition is only checked if specific status code conditions are *not* set. 174 | 175 | 176 | ## Contribution 177 | 178 | Welcome to submit issues and pull requests to improve ComfyUI-RequestNodes! 179 | 180 | --- 181 | 182 | **Note:** 183 | 184 | * Please ensure that your ComfyUI environment has Git installed correctly. 185 | * If your ComfyUI installation directory is not in the default location, please adjust the path according to your actual situation. 186 | * If you encounter any problems, please check the issue page of the GitHub repository or submit a new issue. 187 | --------------------------------------------------------------------------------