feat: add request.interact, request.post, and cookie injection#314
feat: add request.interact, request.post, and cookie injection#314troshab wants to merge 1 commit into
Conversation
896e74a to
ddeec35
Compare
|
Interact doesn't seem to work at all, always times out using the same sequence of actions I had on a standalone Playwright script for a login form before it had a challenge. |
Hey @BrutuZ, thanks for testing this out! I'm actively using request.interact in another project (automated login flow with fill/click/type/solve_turnstile), so I'm interested in getting to the bottom of this. A few things that could help debug:
One workaround I use for tricky submit buttons: {"action": "js", "value": "document.querySelector('button[type=submit]').click()"} instead of the native click action - it bypasses any Playwright-level interaction issues. |
def get_cookies(
url: str,
method: 'Literal["get", "post", "interact"]' = 'get',
post_data: dict[str, str] | None = None,
interactions: list[Interaction] | None = None,
) -> list[Cookie]:
cmd = {
'cmd': f'request.{method}',
'url': url,
# 'return_only_cookies': True,
'cookies': browser_state().get('cookies', []),
# 'maxTimeout': 60000
}
if post_data is not None:
cmd['post_data'] = '&'.join([f'{quote(k)}={quote(v)}' for k, v in post_data.items()])
if interactions is not None:
cmd['interactions'] = interactions
for _ in range(3):
try:
response: SolverResponse = requests.post(
url='http://localhost:8191/v1',
headers={'Content-Type': 'application/json'},
json=cmd,
timeout=30,
).json()
except requests.RequestException as e:
response = {'status': 'error', 'message': 'No response'}
print('Error, retrying', e)
continue
break
get_cookies(
url,
method='interact',
interactions=[
{'action': 'fill', 'selector': 'input[name="UserName"]', 'value': user},
{'action': 'fill', 'selector': 'input[name="PassWord"]', 'value': pwd},
{'action': 'click', 'selector': 'input[name="ipb_login_submit"]'},
# {'action': 'solve_turnstile'},
{'action': 'wait', 'selector': 'div[id=nb]'},
],
)
|
Found it! The field name is actions, not interactions: cmd['interactions'] = interactions # <- silently ignored by Pydantic should be:cmd['actions'] = interactions Pydantic V2 ignores unknown fields by default, so interactions was quietly dropped and actions defaulted to [] - the action loop never Totally fair mistake - the command is request.interact, the model is InteractAction, so interactions feels like the natural field name. Re: logger not outputting - Byparr uses loguru, so try LOGURU_LEVEL=DEBUG instead of LOG_LEVEL. Let me know if it works after renaming the field! |
I feel stupid for overlooking this, I implemented the new methods from the PR looking at the code and models 🤦♂️That's what I get for not being lazy enough to copy/paste 😂
Just followed what I saw there. Line 33 in ddeec35 Line 11 in ddeec35 Took a few restarts but it's logging as expected, so along with the field name everything is working as intended \o/ |
ThePhaseless
left a comment
There was a problem hiding this comment.
Overall Great work! But I see some code quality tweaks that should be applied, see comments.
If you have any questions/comments/objections about them, do not hesitate to comment!
- Add request.interact command with browser actions (fill, click, type, wait, js, screenshot, solve_turnstile) - Add request.post command with form data support - Add cookie injection into browser context before navigation - Add return_only_cookies option to omit response body - Refactor into extracted helper functions for readability - Use match/case for command routing and action dispatch - Use contextlib.suppress for non-critical networkidle timeouts - Add cookie validation (require domain or url) - Add generic exception handler returning JSON instead of bare 500
5c63a57 to
b6d543f
Compare
There was a problem hiding this comment.
Pull request overview
This PR expands the /v1 API to support additional FlareSolverr-compatible commands beyond simple GET navigation, including POST requests, cookie injection, and scripted “interact” browser action sequences.
Changes:
- Added
InteractActionplus newLinkRequestfields (cookies,post_data,return_only_cookies,actions) to supportrequest.postandrequest.interact. - Refactored endpoint logic into helpers for navigation/load waiting, challenge solving, and action execution.
- Added optional response-body suppression via
return_only_cookies, plus basic logging truncation for JS evaluation output.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 8 comments.
| File | Description |
|---|---|
src/models.py |
Adds request schema fields for cookies/POST/interact actions and introduces InteractAction. |
src/endpoints.py |
Implements request.post + request.interact, cookie injection, new action execution helpers, and updated error handling/logging. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| with contextlib.suppress(TimeoutError): | ||
| await page.wait_for_load_state( | ||
| "networkidle", timeout=timer.remaining() * 1000 | ||
| ) |
| status_code=408, | ||
| detail="Timed out while solving the challenge", | ||
| ) from e | ||
| status = HTTPStatus.OK |
| # Playwright requires each cookie to have either url or domain+path | ||
| if request.cookies: | ||
| valid_cookies = [c for c in request.cookies if c.get("domain") or c.get("url")] |
| api_resp = await dep.context.request.post( | ||
| request.url, | ||
| form={ | ||
| k: v | ||
| for pair in request.post_data.split("&") | ||
| for k, v in [pair.split("=", 1)] | ||
| } |
| case "screenshot": | ||
| path = act.value or "/tmp/screenshot.png" # noqa: S108 | ||
| await page.screenshot(path=path, full_page=True) | ||
| logger.info(f"Screenshot saved to {path}") |
| logger.error("Timed out while solving the challenge") | ||
| raise HTTPException( | ||
| status_code=408, | ||
| detail="Timed out while solving the challenge", |
| class InteractAction(BaseModel): | ||
| action: str = Field(description="Action type: 'fill', 'click', 'type', 'wait', 'wait_url', 'solve_turnstile'") | ||
| selector: str | None = None | ||
| value: str | None = None | ||
| timeout: int = Field(default=30000, description="Timeout in milliseconds") |
|
@troshab check AIs comments - most of them are probably false-positives, but I have never used the functionality that this RP adds so just to make sure. Also I'll try to figure out if this could be simplified |
Summary
Extends Byparr with three new request types and cookie management:
request.interact: Navigate to URL, solve Cloudflare challenge, then execute sequential browser actions (fill, click, type, wait, wait_url, solve_turnstile, js, screenshot, sleep). Enables login flows and form submissions through anti-bot-protected pages.request.post: API-level POST with form data viacontext.request.post(). Uses browser context cookies but skips page rendering for speed.context.add_cookies()before navigation. Enables session reuse across requests.return_only_cookies: Skip response body when only cookies are needed (e.g. after login).New models
InteractAction: Sequential browser action withaction,selector,value,timeoutfieldsLinkRequestextended:cookies,post_data,return_only_cookies,actionsfieldscmdfield now supportsrequest.get,request.post,request.interactTurnstile solver (inside
request.interact)Three fallback methods for Cloudflare Turnstile challenges embedded in login pages:
playwright_captchasolver (slowest, last resort)Backwards compatibility
All new fields have defaults - existing
request.getbehavior is unchanged. Thecmdfield description is updated but the default value remainsrequest.get.