PHPackages                             ndrstmr/icap-flow - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [HTTP &amp; Networking](/categories/http)
4. /
5. ndrstmr/icap-flow

ActiveLibrary[HTTP &amp; Networking](/categories/http)

ndrstmr/icap-flow
=================

State-of-the-art, async-ready ICAP client for PHP.

v3.0.0(2mo ago)211EUPL-1.2PHPPHP ^8.4CI passing

Since Jun 18Pushed 2mo agoCompare

[ Source](https://github.com/ndrstmr/icap-flow)[ Packagist](https://packagist.org/packages/ndrstmr/icap-flow)[ RSS](/packages/ndrstmr-icap-flow/feed)WikiDiscussions main Synced today

READMEChangelog (5)Dependencies (17)Versions (13)Used By (0)

 [![IcapFlow Logo](docs/assets/IcapFlow-logo.svg)](docs/assets/IcapFlow-logo.svg)

[![Latest Stable Version](https://camo.githubusercontent.com/a52cc0855218f2ec890b1250784eac2dcab81c9cdba1057ae6b1dd75f5f534e4/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6e647273746d722f696361702d666c6f77)](https://packagist.org/packages/ndrstmr/icap-flow)[![Total Downloads](https://camo.githubusercontent.com/c3d75895ed3fb878e50fe8aad44413cb528cfd6950c6a98a61c3029ede666347/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6e647273746d722f696361702d666c6f77)](https://packagist.org/packages/ndrstmr/icap-flow)[![GitHub Actions Workflow Status](https://camo.githubusercontent.com/71886b2add425bf94bde9d9640d719d7bb4b9edffc2797174868d52a22f8a9ef/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6e647273746d722f696361702d666c6f772f63692e796d6c3f6272616e63683d6d61696e)](https://github.com/ndrstmr/icap-flow/actions)[![Code Coverage](https://camo.githubusercontent.com/493217a03a22730326e5f71e8d8933eb40fb3e2f3a51b5e68a54b0d707aa5b9c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f436f6465253230436f7665726167652d566965772532305265706f72742d626c75652e737667)](https://ndrstmr.github.io/icap-flow/)[![PHPStan Level](https://camo.githubusercontent.com/b72adb1f27170ecf486459c4b07e920bb3db2b464444bce8277e018270665646/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048505374616e2d6c6576656c253230392d627269676874677265656e)](https://phpstan.org/)[![License](https://camo.githubusercontent.com/d392af71f92381cf5295c0ca95d205df21c9a056111959fb69a4d397e6b5325f/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f6e647273746d722f696361702d666c6f77)](https://github.com/ndrstmr/icap-flow/blob/main/LICENSE)

icap-flow
=========

[](#icap-flow)

An async-ready ICAP (Internet Content Adaptation Protocol) client for PHP 8.4+, focused on RFC 3507 correctness, fail-secure semantics and a small surface area that is comfortable to drop into a Symfony / Laravel / framework-less code base.

Warning

**AI-assisted origin &amp; production-use disclaimer.** Large parts of this library — code, tests, docs and CI plumbing — were authored with substantial AI assistance, captured under three independent due-diligence reviews in [`docs/review/`](docs/review/). The v2 line closes the protocol- and security-blocking findings of those reviews, but a piece of code that scans uploads for malware sits in the security-critical path of any application that uses it. **Do not deploy this in production without a deep, independent review and an integration-test bake-out against the ICAP server you actually use.** A non-exhaustive checklist:

- End-to-end test against your production ICAP vendor (c-icap, Symantec, Trend Micro, McAfee Web Gateway, Sophos, Kaspersky, …) — wire formats vary in subtle ways.
- Fail-secure verification: confirm an unreachable / 5xx / malformed-response server makes your application **block the upload**, not silently pass it through.
- TLS configuration review (cipher policy, hostname verification, cert pinning where appropriate).
- Resource limits (`Config::withLimits(...)`) tuned to your traffic profile.
- Audit logging via PSR-3 wired into your central log pipeline.

This software is provided AS IS under EUPL-1.2; the licence's "no warranty" clauses apply unconditionally.

What changed in v3
------------------

[](#what-changed-in-v3)

`v3.0.0` is a small, deliberate breaking release. It carries no new features — it is a cleanup pass that closes three known-stale corners of the v2 API so the surface can be frozen for the upcoming Symfony bundle (`ndrstmr/icap-flow-bundle`):

- **`IcapClient::executeRaw()` is now `protected`.** It was always meant as an internal seam for the preview flow and never appeared in `IcapClientInterface`. Keeping it public let external callers bypass the fail-secure status-code interpretation. External callers must use `request()`, `scanFile()`, `scanFileWithPreview()` or `options()`; subclasses keep raw access.
- **`options()` returns `Future`** (was `Future`). OPTIONS is capability discovery, not a virus verdict — callers want the headers (`Preview`, `Options-TTL`, `Methods`, `Allow`, `Service`, `ISTag`, `Max-Connections`) directly. Fail-secure is preserved: 4xx still throws `IcapClientException`, 5xx still throws `IcapServerException`, `100 Continue` still throws `IcapProtocolException` — extracted into a single `assertSuccessfulStatus()` helper shared between `interpretResponse()` and `options()`.
- **`IcapResponseException` is removed.** Deprecated since v2.0 (`#[\Deprecated]` since v2.2). Both throw sites (`IcapClient::interpretResponse()` backstop, `DefaultPreviewStrategy::handlePreviewResponse()` `default` branch) now throw `IcapProtocolException`. Callers using `catch (IcapExceptionInterface $e)` are unaffected.

Migration: [`docs/migration-v2-to-v3.md`](docs/migration-v2-to-v3.md). For most call sites the upgrade is a no-op other than bumping the constraint to `^3.0`.

What changed in v2
------------------

[](#what-changed-in-v2)

`v2.0.0` was a **breaking** release that fixes RFC-3507-blocking bugs in v1. The v1 line is **deprecated**. Highlights:

- **RFC-3507 wire format** is correct: real `Encapsulated` offsets, HTTP-in-ICAP nesting, chunked bodies for both string and stream payloads, `0; ieof` terminator on preview-complete.
- **Streaming-safe preview** — `scanFileWithPreview()` no longer buffers the file; only the preview window is read.
- **Fail-secure on status 100** — a stray 100 outside the preview flow now throws `IcapProtocolException` instead of silently mapping to a clean scan.
- **CRLF / header-injection guard** on `$service` and on user-supplied ICAP headers.
- **TLS / icaps://** support via amphp's `ClientTlsContext`.
- **DoS limits** — `maxResponseSize`, `maxHeaderCount`, `maxHeaderLineLength`.
- **Full status-code matrix** — 4xx → `IcapClientException`, 5xx → `IcapServerException`, 206 inspected, …
- **Multi-vendor virus headers** — Config takes an ordered list (`X-Virus-Name`, `X-Infection-Found`, `X-Violations-Found`, `X-Virus-ID`).
- **PSR-3 logger** optional, structured events on every request.
- **Custom request headers** (`X-Client-IP`, `X-Authenticated-User`) on `scanFile()` / `scanFileWithPreview()`.
- **External cancellation** — every public method takes an optional `Amp\Cancellation`.
- **OPTIONS-response cache** with `Options-TTL` honour.
- **`RetryingIcapClient`** decorator with exponential backoff for 5xx.
- **Encapsulated-aware response framing** — no dependency on `Connection: close`; servers may keep the socket open.
- **PHP 8.4** minimum; **PHP 8.5** in CI; integration tested end-to-end against `mnemoshare/clamav-icap` (c-icap 0.6.3 + ClamAV).

The migration guide is [`docs/migration-v1-to-v2.md`](docs/migration-v1-to-v2.md). The full per-finding closure list is in [`docs/review/consolidated_task-list.md`](docs/review/consolidated_task-list.md).

> **v2.1.0** added keep-alive connection pooling and strict RFC 3507 §4.5 preview-continue (preview + continuation on the same socket). **v2.2.0** added OPTIONS-driven pool tuning (`Max-Connections`), pool idle eviction, ISTag-based cache invalidation, PSR-6/PSR-16 OPTIONS-cache adapters and a per-IO timeout model. See [`CHANGELOG.md`](CHANGELOG.md).

Installation
------------

[](#installation)

```
composer require ndrstmr/icap-flow:^3.0
```

Quickstart — synchronous
------------------------

[](#quickstart--synchronous)

```
use Ndrstmr\Icap\SynchronousIcapClient;

$icap = SynchronousIcapClient::create();

$result = $icap->scanFile('/avscan', '/path/to/upload.bin');

echo $result->isInfected()
    ? 'Virus found: ' . $result->getVirusName() . PHP_EOL
    : 'File is clean' . PHP_EOL;
```

Quickstart — asynchronous (amphp v3 / Revolt)
---------------------------------------------

[](#quickstart--asynchronous-amphp-v3--revolt)

```
use Ndrstmr\Icap\IcapClient;
use Revolt\EventLoop;

$icap = IcapClient::create();

EventLoop::run(function () use ($icap) {
    $future = $icap->scanFile('/avscan', '/path/to/upload.bin');
    $result = $future->await();

    echo $result->isInfected()
        ? 'Virus: ' . $result->getVirusName() . PHP_EOL
        : 'Clean' . PHP_EOL;
});
```

Configuration
-------------

[](#configuration)

```
use Amp\Socket\ClientTlsContext;
use Ndrstmr\Icap\Config;
use Ndrstmr\Icap\IcapClient;
use Ndrstmr\Icap\RequestFormatter;
use Ndrstmr\Icap\ResponseParser;
use Ndrstmr\Icap\Transport\AsyncAmpTransport;

$config = (new Config(
    host: 'icap.example.com',
    port: 11344,                 // 1344 is plain ICAP, 11344 is the de-facto TLS port
    socketTimeout: 5.0,
    streamTimeout: 30.0,
))
    ->withTlsContext(new ClientTlsContext('icap.example.com'))
    ->withVirusFoundHeaders([
        'X-Virus-Name',          // ClamAV / c-icap
        'X-Infection-Found',     // ISS Proventia
        'X-Violations-Found',    // Trend Micro
        'X-Virus-ID',            // Symantec
    ])
    ->withLimits(
        maxResponseSize: 10 * 1024 * 1024,
        maxHeaderCount: 100,
        maxHeaderLineLength: 8192,
    );

$client = new IcapClient(
    $config,
    new AsyncAmpTransport(),
    new RequestFormatter(),
    new ResponseParser(
        maxHeaderCount: $config->getMaxHeaderCount(),
        maxHeaderLineLength: $config->getMaxHeaderLineLength(),
    ),
    null,                        // PreviewStrategyInterface (DefaultPreviewStrategy if null)
    $logger,                     // Psr\Log\LoggerInterface (NullLogger if null)
);
```

### Connection pool

[](#connection-pool)

For long-running workers (RoadRunner, Swoole, ReactPHP) the async transport can reuse sockets via a connection pool:

```
use Ndrstmr\Icap\Cache\InMemoryOptionsCache;
use Ndrstmr\Icap\Transport\AmpConnectionPool;
use Ndrstmr\Icap\Transport\AsyncAmpTransport;

$pool = new AmpConnectionPool(
    maxConnectionsPerHost: 8,    // idle-socket cap per host:port:tls
    maxIdleSeconds: 30.0,        // evict sockets idle longer than 30 s
);

$transport = new AsyncAmpTransport($pool);

// Optional: pass an OPTIONS cache so the client auto-negotiates
// preview size and honours Options-TTL / ISTag invalidation.
$cache = new InMemoryOptionsCache();
// For cross-process caching (Redis, APCu) use Psr16OptionsCache
// or Psr6OptionsCache instead.

$client = new IcapClient($config, $transport, new RequestFormatter(), new ResponseParser(), optionsCache: $cache);
```

Custom request headers
----------------------

[](#custom-request-headers)

```
$result = $icap->scanFile('/avscan', '/path/to/upload.bin', [
    'X-Client-IP'          => '203.0.113.5',
    'X-Authenticated-User' => base64_encode('user@example.org'),
]);
```

Header names and values are validated against CR / LF / NUL / control characters — injection attempts raise `InvalidArgumentException` before any byte hits the socket. Library-managed headers (`Encapsulated`, `Host`, `Connection`, and inside the preview flow `Preview` / `Allow`) always take precedence over caller-supplied values.

Exception taxonomy
------------------

[](#exception-taxonomy)

Every exception this library throws implements `Ndrstmr\Icap\Exception\IcapExceptionInterface` so you can catch the whole family in one block.

ExceptionTrigger`IcapConnectionException`TCP-level failure (refused, timeout, TLS handshake, ...)`IcapTimeoutException`Stream cancellation timed out`IcapProtocolException`RFC-3507 violation (e.g. status 100 outside preview, malformed `Encapsulated`)`IcapMalformedResponseException` (extends `IcapProtocolException`)Server response can't be parsed (no separator, header line without `:`, oversize lines)`IcapClientException`ICAP 4xx response — request rejected by server, code is the real status`IcapServerException`ICAP 5xx response — server failed, code is the real status`IcapResponseException`Status code that doesn't fit any other bucketExamples
--------

[](#examples)

The `examples/` directory has runnable demos, including a full Symfony-ready async cookbook:

- `examples/01-sync-scan.php` — minimal synchronous scan
- `examples/02-async-scan.php` — async scan inside `Revolt\EventLoop`
- `examples/cookbook/01-custom-headers.php` — `X-Client-IP`, `X-Authenticated-User`
- `examples/cookbook/02-custom-preview-strategy.php` — vendor-specific preview interpretation
- `examples/cookbook/03-options-request.php` — capability discovery via OPTIONS
- `examples/cookbook/04-tls-mtls.php` — TLS and mutual TLS (mTLS) setup
- `examples/cookbook/05-retry-decorator.php` — exponential-backoff retry on 5xx
- `examples/cookbook/06-pool-tuning.php` — connection-pool idle eviction and Max-Connections
- `examples/cookbook/07-cancellation-from-upload.php` — timeout and user-initiated cancellation

Integration tests
-----------------

[](#integration-tests)

A docker-compose stack (`docker-compose.yml`) brings up [`mnemoshare/clamav-icap`](https://hub.docker.com/r/mnemoshare/clamav-icap) on port 1344. The tests in `tests/Integration/` skip when `ICAP_HOST` is unset, so contributors without Docker get a green `composer test:integration` while CI exercises a real wire-level round trip on every PR.

```
docker compose up -d
ICAP_HOST=127.0.0.1 ICAP_PORT=1344 \
  ICAP_ECHO_SERVICE=/avscan \
  ICAP_CLAMAV_SERVICE=/avscan \
    composer test:integration
```

Provenance &amp; due diligence
------------------------------

[](#provenance--due-diligence)

`docs/review/` carries the three independent due-diligence reports (Claude, Codex, Jules) that drove the v2 redesign, and a verified consolidated task list. They are part of the repo, not after-the-fact marketing — every closed finding maps back to a specific file/line in those docs.

Developers
----------

[](#developers)

```
composer test           # unit suite (Pest)
composer test:integration   # against a configured ICAP server
composer stan           # PHPStan level 9 + bleedingEdge
composer cs-check       # PSR-12 (php-cs-fixer)
composer cs-fix         # apply fixes
composer audit          # composer + roave/security-advisories
```

CI matrix: PHP 8.4 + 8.5. See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the PR workflow.

Licence
-------

[](#licence)

EUPL-1.2 — see [`LICENSE`](LICENSE). The licence is OpenCoDE-compatible and explicitly designed for European public-sector software.

Security
--------

[](#security)

To report a vulnerability, see [`SECURITY.md`](SECURITY.md). Please **do not** open a public GitHub issue for security findings.

Changelog
---------

[](#changelog)

[`CHANGELOG.md`](CHANGELOG.md) — Keep a Changelog format, SemVer-committed.

###  Health Score

43

—

FairBetter than 89% of packages

Maintenance88

Actively maintained with recent releases

Popularity5

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity61

Established project with proven stability

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~53 days

Recently: every ~2 days

Total

7

Last Release

64d ago

Major Versions

v1.0.0 → v2.0.02026-04-25

v2.2.0 → v3.0.02026-05-01

PHP version history (2 changes)v1.0.0PHP &gt;=8.3

v2.0.0PHP ^8.4

### Community

Maintainers

![](https://www.gravatar.com/avatar/ec966b63930eb41af981e8daf965073bc19f42bc18f489d80ff420050a7a624e?d=identicon)[ndrstmr](/maintainers/ndrstmr)

---

Top Contributors

[![ndrstmr](https://avatars.githubusercontent.com/u/153233004?v=4)](https://github.com/ndrstmr "ndrstmr (64 commits)")

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/ndrstmr-icap-flow/health.svg)

```
[![Health](https://phpackages.com/badges/ndrstmr-icap-flow/health.svg)](https://phpackages.com/packages/ndrstmr-icap-flow)
```

###  Alternatives

[symfony/http-kernel

Provides a structured process for converting a Request into a Response

8.1k869.4M8.8k](/packages/symfony-http-kernel)[amphp/http-server

A non-blocking HTTP application server for PHP based on Amp.

1.3k6.7M110](/packages/amphp-http-server)[symfony/http-client

Provides powerful methods to fetch HTTP resources synchronously or asynchronously

2.0k338.8M5.0k](/packages/symfony-http-client)[zircote/swagger-php

Generate interactive documentation for your RESTful API using PHP attributes (preferred) or PHPDoc annotations

5.3k144.5M608](/packages/zircote-swagger-php)[drupal/core

Drupal is an open source content management platform powering millions of websites and applications.

21866.0M1.7k](/packages/drupal-core)[danog/madelineproto

Async PHP client API for the telegram MTProto protocol.

3.5k902.0k23](/packages/danog-madelineproto)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
