Skip to content
Hack Your WorldSoftware · Infrastructure · Home automation

Analysis

A 174-Line Temporary PHP Fix Reached 20 Million Downloads. Now It’s Everyone Else’s Migration.

In 2014, Jake Smith wrote 174 lines of PHP so an AOL content-management system could move from PHP 5.2 to 5.3 without changing dozens of URL-building calls. The replacement function was supposed to last a year or two.

A small software component spreading across packages, servers, and websites
AI image: Hack Your World

Twelve years later, jakeasmith/http_build_url was marked abandoned. On October 10, 2026, Packagist listed 20,195,532 installs and version 1.0.2, released September 15. That is a dated package count, not a live counter on this page.

Packagist downloads are not 20 million distinct production installations. CI systems and repeated Composer installs count too. The number still tells me the package is moving through an enormous amount of software for something its author considered a stopgap.

The more important number may be the installations Packagist cannot count.

In this article

The dependency escaped the dependency manager

Smith published the package as a polyfill for http_build_url(), a function supplied by version 1 of the pecl_http extension. His implementation defines the function only when it does not already exist, allowing old application code to continue without knowing which implementation handled the request.

That is a good compatibility technique. It is also how temporary code becomes load-bearing. The call sites stay unchanged, the migration succeeds, and the reason the shim exists fades out of operational memory.

The package spread through normal Composer dependencies, including the idna-convert domain-name library. It consequently appears in SPIP’s source and as packages in Debian and Ubuntu. WPML took another route: according to Smith, the WordPress multilingual plugin bundles the polyfill directly and says it runs on more than 1.5 million sites.

Bundling matters. Composer can warn that a named package is abandoned. It cannot warn about source copied into a plugin years ago unless that product’s maintainer carries the message downstream. A software-composition scanner may identify the copied code if it has a good fingerprint; a dependency list generated from composer.lock may not show it at all.

I have maintained enough PHP applications to distrust the idea that composer show describes the whole runtime. WordPress plugins carry vendor trees, distributions ship patched source, applications keep compatibility files outside Composer, and old deployment processes sometimes copy only selected directories. The manifest is evidence, not ground truth.

A bug that removes every “a” is funny until it changes a URL

One known defect explains why “it has worked for years” is weak evidence.

The polyfill appends an a to the base path, then removes every occurrence of that path’s basename using str_replace(). When the original base path ends in a slash, the basename is just a. That removes lowercase a characters throughout the base path. The relative component is appended afterward, so its characters are not removed by that replacement.

Reproduced October 10: I ran four synthetic cases against the version 1.0.2 source in a standalone PHP 8.2.34 process, without loading WordPress or making URL requests. After loading that polyfill file, this call:

echo http_build_url(
    'https://example.test/banana/',
    ['path' => 'apple'],
    HTTP_URL_JOIN_PATH
);
// Observed: https://example.test/bnn/apple

Changing the base to /banana/file produced /banana/apple. Supplying an absolute replacement path, /apple, produced /apple. A fourth case, /pear/ plus apple, produced /per/apple. These are small regression cases for this defect, not evidence that a replacement library preserves every flag or query-string behavior.

Smith is not fixing it. The package will remain installable, but version 1.0.2 marks it abandoned and the README directs users to PHP 8.5’s built-in URI API or league/uri on older PHP versions.

Leaving the bug in place sounds uncomfortable, but changing a twelve-year-old URL function used through unknown call patterns is not obviously safer. Some applications may have tests that encode the current behavior. Others may depend on edge cases that the package never specified. A one-line repair can be correct in isolation and still break downstream systems nobody can enumerate.

Deprecation is the honest state: the implementation is no longer a maintained foundation, existing releases remain available, and downstream owners have to choose when and how to leave.

Smith asked for a successor in 2021 and received volunteers. A death in his family interrupted the process, he moved away from PHP work, and the handoff never happened. That human history is part of the dependency graph even though no lock file can express it.

It would be easy to turn that into a lecture about maintainers owing the ecosystem a succession plan. I do not think that is useful. The package was free, permissively licensed, small, and public. Its author did not force millions of downstream downloads or promise lifetime maintenance.

There is also real risk in transferring a widely used package simply to make the maintenance status look healthier. A new release from a trusted package name runs automatically in build systems around the world. Ownership is therefore a security boundary. A hurried handoff can be more dangerous than a clearly abandoned package whose hash and behavior are known.

The safe options are slower: downstream projects can fork and pin the code they are prepared to own, migrate to a supported URI implementation, or remain on the final release while accepting and documenting the risk. None produces the comforting fiction that one new volunteer has inherited everyone else’s operational responsibility.

Finding it requires more than one command

I would split the inventory into managed, transitive, and copied dependencies.

For Composer projects, composer why jakeasmith/http_build_url identifies the package path when it is present in the resolved graph. composer audit and abandoned-package reporting should be part of CI, but the current lock file and the exact Composer version used by production need to be included. A stale build image can preserve an older result long after a developer workstation has moved on.

Then I would search the deployed source itself, including plugin and vendor directories, for function http_build_url, HTTP_URL_JOIN_PATH, and HTTP_URL_JOIN_QUERY. That can find direct copies Composer cannot name, but renamed functions, reformatted constants, generated files, or code outside the searched deployment tree can evade a text search.

On WordPress, I would search the actual plugin release installed in production rather than assume its upstream repository matches the deployed artifact. On Debian or Ubuntu, I would also query the package database for php-jakeasmith-http-build-url and inspect reverse dependencies before removal.

Finally, I would trace behavior. Search for every call, record which flags are used, and build tests from real URLs before replacing the function. URI resolution has enough edge cases—relative paths, empty queries, fragments, encoding, ports, authority components, and duplicate query keys—that a syntactically simple replacement can change output.

PHP 8.5 provides an exit, not an automatic migration

PHP 8.5 now includes a standards-based URI API under Uri\Rfc3986\Uri. It offers a native migration target; upgrading PHP does not rewrite existing http_build_url() calls or reproduce all their flag semantics. Applications on older PHP versions can use a maintained library such as league/uri, or implement a narrow replacement with parse_url() and http_build_query() when their requirements are genuinely simple.

None is a drop-in promise for every combination of flags supported by http_build_url(). The migration should compare outputs against application expectations, not merely replace one function name with another.

The package’s own origin is the caution: it existed because changing dozens of call sites during a PHP upgrade looked riskier than recreating an old function. That decision was reasonable in 2014. The mistake would be treating its success as proof that the temporary layer no longer needs an owner.

Keep the failing case in the migration suite, but do not treat the old output as the required answer. Decide which behavior the application needs, especially for relative paths and repeated query keys, and document intentional changes before replacing the implementation.

Sources