{{ message }}
Repository navigation
Conversation
Uploads a JPG, PNG, or WebP image to POST /image as multipart/form-data and returns the decoded response, whose image_id can be passed to Google Lens and other engines that accept uploaded images. Mirrors upload_image in serpapi-ruby and serpapi-python: - accepts a file path or a stream; seekable streams are rewound first, as Ruby does, and uploaded from a temporary copy because cURL uploads by path and CURLStringFile needs PHP 8.1 - sends the same parameters as any other request (api_key, source, default search parameters) as form fields, so the key stays out of the URL; per-call api_key and timeout work as for search() - sends only the file's base name; without one, cURL would send the full local path - errors raise SerpApiException with the same context as searches The persistent cURL handle now resets the method on every GET. Without CURLOPT_HTTPGET, a search after an upload went out as a multipart POST. Uploads also drop the Expect: 100-continue header: libcurl before 7.69 sends it with bodies over 1 KB, SerpApi does not answer it, and each upload stalled for a second. The live integration test uploads a 27 KB public domain thumbnail of the Mona Lisa from Wikimedia Commons and searches it with Google Lens.
galetahub
requested changes
Oct 21, 2026
Comment on lines
+380
to
+382
Contributor
There was a problem hiding this comment.
An empty or already-consumed stream (e.g. a pipe that was read before) passes this check: stream_copy_to_stream returns 0, not false.
Suggested change
| if (stream_copy_to_stream($stream, $copy) === false || !fflush($copy)) { | |
| fclose($copy); | |
| throw new SerpApiException('Failed to read the image stream'); | |
| $copied = stream_copy_to_stream($stream, $copy); | |
| if ($copied === false || $copied === 0 || !fflush($copy)) { | |
| fclose($copy); | |
| throw new SerpApiException('Failed to read the image stream'); |
| { | ||
| $fields = []; | ||
|
|
||
| foreach (explode('&', http_build_query($query)) as $pair) { |
Contributor
There was a problem hiding this comment.
Nit: http_build_query uses the arg_separator.output ini setting. Passing '&' explicitly removes the php.ini dependency.
Suggested change
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Adds
Client::uploadImage()for the Image API, matchingupload_imagein serpapi-ruby (#27) and serpapi-python (#38), anduploadImagein serpapi-javascript (#46).Behavior
fopen($path, 'rb')orphp://memory.CURLStringFileneeds PHP 8.1.multipart/form-dataPOST to/image.api_key,source, default search parameters), the way Ruby'spostreusesquery(params).api_keyandtimeoutwork as they do forsearch(), andnulldrops a default.message,image_id).SerpApiExceptionwith the same context as a search error, without the key or the file.Fixes needed in the shared cURL handle
CURLOPT_HTTPGET. Without it, the search that follows an upload goes out as a multipart POST. I confirmed both before and after the fix against a local echo server.Expectheader: uploads sendExpect:to dropExpect: 100-continue. libcurl before 7.69 adds that header to bodies over 1 KB, SerpApi doesn't answer it, and each upload stalled for about 1 s. I measured this by forcing the header against the live API.request()gains an optional$formargument. It is@internal, and the test doubleRecordingClientwas updated to match. The JSON parse error now reads "from url" instead of "on get url", as Ruby's does and as the HTTP error already did.Tests
ClientUploadImageTest(offline) covers:api_key,timeoutandnull;ClientIntegrationTest::testUploadImage(live) uploadstests/fixtures/mona-lisa.jpg(a 27 KB public domain thumbnail from Wikimedia Commons). It then runs a Google Lens search forvisual_matcheson the same persistent connection.Locally on PHP 8.5:
ClientIntegrationTestand the Google Lens example pass live;phpcsand PHPStan level 6 are clean.A live stream upload also succeeded, and an invalid file returns the API's error message.
Follow-ups (not in this PR)
VERSIONto1.1.0with a change log entry that includes this method. This replaces Bump version to 1.1.0 #6, which targets the mergedfeature/ruby-paritybranch and lists Markdown as new, although Markdown is already in 1.0.0.release.yml: it passesAPI_KEY, but the tests readSERPAPI_KEYsince chore: rename API_KEY env var to SERPAPI_KEY #4, so thev1.0.0release ran with every live test skipped.