Skip to content

feat: add request.interact, request.post, and cookie injection#314

Open
troshab wants to merge 1 commit into
ThePhaseless:mainfrom
troshab:feature/interact-post-cookies
Open

feat: add request.interact, request.post, and cookie injection#314
troshab wants to merge 1 commit into
ThePhaseless:mainfrom
troshab:feature/interact-post-cookies

Conversation

@troshab

@troshab troshab commented Feb 11, 2026

Copy link
Copy Markdown

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 via context.request.post(). Uses browser context cookies but skips page rendering for speed.
  • Cookie injection: Pre-load cookies via 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 with action, selector, value, timeout fields
  • LinkRequest extended: cookies, post_data, return_only_cookies, actions fields
  • cmd field now supports request.get, request.post, request.interact

Turnstile solver (inside request.interact)

Three fallback methods for Cloudflare Turnstile challenges embedded in login pages:

  1. Click iframe bounding box on parent page (fastest)
  2. Click inside CF frame elements
  3. playwright_captcha solver (slowest, last resort)

Backwards compatibility

All new fields have defaults - existing request.get behavior is unchanged. The cmd field description is updated but the default value remains request.get.

@troshab troshab force-pushed the feature/interact-post-cookies branch 3 times, most recently from 896e74a to ddeec35 Compare February 11, 2026 17:44
@troshab troshab changed the title feat: add request.interact, request.post, cookie injection, and binary content support feat: add request.interact, request.post, and cookie injection Feb 11, 2026
@cpiber cpiber mentioned this pull request Feb 19, 2026
1 task
@BrutuZ

BrutuZ commented Mar 2, 2026

Copy link
Copy Markdown

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.
I tried toggling the headless mode and indeed, no text fields get filled and no button clicked (which submits the form via POST to a subdomain guarded by the challenge).

@troshab

troshab commented Mar 2, 2026

Copy link
Copy Markdown
Author

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. I tried toggling the headless mode and indeed, no text fields get filled and no button clicked (which submits the form via POST to a subdomain guarded by the 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:

  1. Could you share your actions payload? (redact credentials obviously) - just the JSON structure with action types and selectors.
  2. Does the page have a Cloudflare challenge/Turnstile before the login form loads? The current code solves the interstitial challenge first (checks page title), but if Turnstile is embedded inside the form page itself, the actions might be running against a page that hasn't fully loaded yet.
  3. Are you using wait actions before fill/click? In my working setup I always chain wait -> fill:
    {"action": "wait", "selector": "#loginEmail", "timeout": 30000},
    {"action": "fill", "selector": "#loginEmail", "value": "..."}
  4. Without the explicit wait, SPA pages may not have the elements ready even after domcontentloaded/networkidle.
  5. Could you grab container logs? docker logs - the interact code has debug logging that should show whether actions are reached at all, or if it's stuck on challenge solving / page load.
  6. Quick diagnostic: try adding a screenshot action as the very first action (before any fill/click) to see what the browser actually sees at that point:
    {"action": "screenshot", "value": "/tmp/debug.png"}

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.

@BrutuZ

BrutuZ commented Mar 2, 2026

Copy link
Copy Markdown
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]'},
  ],
)
  1. No challenge on first load, only on submission (the form action is to a protected subdomain), after passing the challenge it is supposed to get the response cookies and redirect you back to the first unprotected domain.
  2. I am not, just relying on the built-in wait from the selectors
  3. It's not a responsive page either, it's plainass HTML, everything is there on load
  4. Running baremetal with UV, no container, but it doesn't help the logger calls don't output anything to the console even with the LOG_LEVEL=DEBUG environment
  5. I disabled the headless mode as a diagnostic, so it actually opens the browser xD
    headless=True,

@troshab

troshab commented Mar 2, 2026

Copy link
Copy Markdown
Author
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]'},
  ],
)
  1. No challenge on first load, only on submission (the form action is to a protected subdomain), after passing the challenge it is supposed to get the response cookies and redirect you back to the first unprotected domain.
  2. I am not, just relying on the built-in wait from the selectors
  3. It's not a responsive page either, it's plainass HTML, everything is there on load
  4. Running baremetal with UV, no container, but it doesn't help the logger calls don't output anything to the console even with the LOG_LEVEL=DEBUG environment
  5. I disabled the headless mode as a diagnostic, so it actually opens the browser xD
    headless=True,

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
ran. That's why nothing happened on screen.

Totally fair mistake - the command is request.interact, the model is InteractAction, so interactions feels like the natural field name.
I just pushed a fix (5c63a57) that adds extra="forbid" to LinkRequest, so now Pydantic will return a clear 422 error on any unrecognized
field instead of silently swallowing it.

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!

@BrutuZ

BrutuZ commented Mar 3, 2026

Copy link
Copy Markdown

Found it! The field name is actions, not interactions:
cmd['interactions'] = interactions # <- silently ignored by Pydantic

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 😂

Re: logger not outputting - Byparr uses loguru, so try LOGURU_LEVEL=DEBUG instead of LOG_LEVEL.

Just followed what I saw there.

logger.setLevel(LOG_LEVEL)

LOG_LEVEL = logging.getLevelNamesMapping()[os.getenv("LOG_LEVEL", "INFO").upper()]

Took a few restarts but it's logging as expected, so along with the field name everything is working as intended \o/

@ThePhaseless ThePhaseless left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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!

Comment thread src/endpoints.py Outdated
Comment thread src/endpoints.py Outdated
Comment thread src/endpoints.py Outdated
Comment thread src/endpoints.py Outdated
Comment thread src/endpoints.py Outdated
Comment thread src/models.py
- 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
@troshab troshab force-pushed the feature/interact-post-cookies branch from 5c63a57 to b6d543f Compare March 10, 2026 23:23
@ThePhaseless ThePhaseless requested a review from Copilot March 18, 2026 19:03

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 InteractAction plus new LinkRequest fields (cookies, post_data, return_only_cookies, actions) to support request.post and request.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.

Comment thread src/endpoints.py
Comment on lines +69 to +72
with contextlib.suppress(TimeoutError):
await page.wait_for_load_state(
"networkidle", timeout=timer.remaining() * 1000
)
Comment thread src/endpoints.py
status_code=408,
detail="Timed out while solving the challenge",
) from e
status = HTTPStatus.OK
Comment thread src/endpoints.py
Comment on lines +296 to +298
# 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")]
Comment thread src/endpoints.py
Comment on lines +231 to +237
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)]
}
Comment thread src/endpoints.py
Comment on lines +136 to +139
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}")
Comment thread src/endpoints.py
Comment on lines +314 to +317
logger.error("Timed out while solving the challenge")
raise HTTPException(
status_code=408,
detail="Timed out while solving the challenge",
Comment thread src/models.py
Comment on lines +14 to +18
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")
Comment thread src/endpoints.py
@ThePhaseless

Copy link
Copy Markdown
Owner

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants