77import re
88from collections .abc import Callable
99from typing import Any
10+ from urllib .parse import urlsplit
1011
1112import httpx
1213
@@ -42,6 +43,7 @@ def __init__(
4243 max_requests_per_minute : int | None = None ,
4344 client : httpx .AsyncClient | None = None ,
4445 model_name : str = "" ,
46+ follow_redirects : bool = True ,
4547 custom_configuration : TargetConfiguration | None = None ,
4648 ** httpx_client_kwargs : Any ,
4749 ) -> None :
@@ -58,6 +60,8 @@ def __init__(
5860 max_requests_per_minute (int, Optional): Maximum number of requests per minute.
5961 client (httpx.AsyncClient, Optional): Pre-configured httpx client.
6062 model_name (str): The model name. Defaults to empty string.
63+ follow_redirects (bool): Whether to follow HTTP redirects. Defaults to True for backward compatibility;
64+ set to False when redirects are unnecessary or the destination must remain fixed.
6165 custom_configuration (TargetConfiguration, Optional): Override the default configuration for
6266 this target instance. Defaults to None.
6367 **httpx_client_kwargs: Additional keyword arguments for httpx.AsyncClient.
@@ -72,6 +76,7 @@ def __init__(
7276 # Parse the URL early to use as endpoint identifier
7377 # This will fail early if the http_request is malformed
7478 _ , _ , endpoint , _ , _ = self .parse_raw_http_request (http_request )
79+ self ._destination_origin = self ._get_destination_origin (endpoint )
7580
7681 super ().__init__ (
7782 max_requests_per_minute = max_requests_per_minute ,
@@ -82,6 +87,7 @@ def __init__(
8287 self .http_request = http_request
8388 self .callback_function = callback_function
8489 self .prompt_regex_string = prompt_regex_string
90+ self .follow_redirects = follow_redirects
8591 self .httpx_client_kwargs = httpx_client_kwargs or {}
8692
8793 if client and httpx_client_kwargs :
@@ -99,6 +105,7 @@ def _build_identifier(self) -> ComponentIdentifier:
99105 "use_tls" : self .use_tls ,
100106 "prompt_regex_string" : self .prompt_regex_string ,
101107 "callback_function" : getattr (self .callback_function , "__name__" , None ),
108+ "follow_redirects" : self .follow_redirects ,
102109 },
103110 )
104111
@@ -110,6 +117,7 @@ def with_client(
110117 prompt_regex_string : str = "{PROMPT}" ,
111118 callback_function : Callable [..., Any ] | None = None ,
112119 max_requests_per_minute : int | None = None ,
120+ follow_redirects : bool = True ,
113121 ) -> "HTTPTarget" :
114122 """
115123 Alternative constructor that accepts a pre-configured httpx client.
@@ -120,6 +128,8 @@ def with_client(
120128 prompt_regex_string: the placeholder for the prompt
121129 callback_function: function to parse HTTP response
122130 max_requests_per_minute: Optional rate limiting
131+ follow_redirects: Whether to follow HTTP redirects. Defaults to True for backward compatibility; set to
132+ False when redirects are unnecessary or the destination must remain fixed.
123133
124134 Returns:
125135 HTTPTarget: an instance of HTTPTarget
@@ -130,6 +140,7 @@ def with_client(
130140 callback_function = callback_function ,
131141 max_requests_per_minute = max_requests_per_minute ,
132142 client = client ,
143+ follow_redirects = follow_redirects ,
133144 )
134145
135146 def _inject_prompt_into_request (self , request : MessagePiece ) -> str :
@@ -142,8 +153,12 @@ def _inject_prompt_into_request(self, request: MessagePiece) -> str:
142153
143154 Returns:
144155 str: the http request with the prompt added in
156+
157+ Raises:
158+ ValueError: If a multiline prompt would be substituted into the request line or headers.
145159 """
146160 re_pattern = re .compile (self .prompt_regex_string )
161+ self ._validate_prompt_for_template_context (prompt = request .converted_value , pattern = re_pattern )
147162 if re .search (self .prompt_regex_string , self .http_request ):
148163 http_request_w_prompt = re_pattern .sub (lambda m : request .converted_value , self .http_request )
149164 else :
@@ -169,6 +184,7 @@ async def _send_prompt_to_target_async(self, *, normalized_conversation: list[Me
169184 http_request_w_prompt = self ._inject_prompt_into_request (request )
170185
171186 header_dict , http_body , url , http_method , http_version = self .parse_raw_http_request (http_request_w_prompt )
187+ self ._validate_destination (url )
172188
173189 if "Content-Length" in header_dict :
174190 header_dict ["Content-Length" ] = str (len (http_body ))
@@ -191,15 +207,15 @@ async def _send_prompt_to_target_async(self, *, normalized_conversation: list[Me
191207 url = url ,
192208 headers = header_dict ,
193209 data = http_body ,
194- follow_redirects = True ,
210+ follow_redirects = self . follow_redirects ,
195211 )
196212 else :
197213 response = await client .request (
198214 method = http_method ,
199215 url = url ,
200216 headers = header_dict ,
201217 content = http_body ,
202- follow_redirects = True ,
218+ follow_redirects = self . follow_redirects ,
203219 )
204220
205221 response_content = response .content
@@ -296,3 +312,27 @@ def _infer_full_url_from_host(
296312
297313 host = headers_dict ["host" ]
298314 return f"{ http_protocol } { host } { path } "
315+
316+ def _validate_destination (self , url : str ) -> None :
317+ destination_origin = self ._get_destination_origin (url )
318+ if destination_origin != self ._destination_origin :
319+ raise ValueError ("Prompt substitution cannot change the configured HTTP destination." )
320+
321+ def _validate_prompt_for_template_context (self , * , prompt : str , pattern : re .Pattern [str ]) -> None :
322+ if "\r " not in prompt and "\n " not in prompt :
323+ return
324+
325+ separator = re .search (r"\r?\n\r?\n" , self .http_request )
326+ header_end = separator .start () if separator else len (self .http_request )
327+
328+ if any (match .start () < header_end for match in pattern .finditer (self .http_request )):
329+ raise ValueError ("Prompts substituted into the HTTP request line or headers cannot contain CR or LF." )
330+
331+ @staticmethod
332+ def _get_destination_origin (url : str ) -> tuple [str , str | None , int | None ]:
333+ parsed_url = urlsplit (url )
334+ try :
335+ port = parsed_url .port
336+ except ValueError as exc :
337+ raise ValueError (f"Invalid port in HTTP destination: { url } " ) from exc
338+ return parsed_url .scheme .lower (), parsed_url .hostname , port
0 commit comments