Skip to content

refactor(client): use IClientService for Etherpad HTTP (#103) - #123

Merged
Jaggob merged 3 commits into
mainfrom
refactor/103-iclientservice
Jun 5, 2026
Merged

refactor(client): use IClientService for Etherpad HTTP (#103)#123
Jaggob merged 3 commits into
mainfrom
refactor/103-iclientservice

Conversation

@Jaggob

@Jaggob Jaggob commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

What

EtherpadClient talked to Etherpad via file_get_contents + stream contexts, bypassing Nextcloud's HTTP client (instance proxy / central TLS + timeout config) and leaving the class untestable. Route all HTTP through OCP\Http\Client\IClientService.

Changes

  • Inject IClientService; replace sendRequest (API calls) and sendPublicGetRequest (version detection). Removed file_get_contents, stream_context_create, $http_response_header, buildHttpContext, extractStatusCode.
  • Redirects stay disabled (allow_redirects => ['max' => 0]) so a redirect can't leak the apikey; timeout stays 15s; Accept: application/json.
  • NC's client throws on ≥400, so doRequest() recovers the real response via getResponseFromThrowable() (NC 29+, we target 31+) to keep status handling at the call site; a response-less throwable is a transport failure.
  • nextcloud.allow_local_address => true for these calls — the Etherpad API host is admin-configured and commonly a loopback/LAN address (e.g. http://localhost:9001). NC's client blocks local addresses by default (SSRF protection for user URLs); without this every API call failed with LocalServerException. User-reachable external pad fetching is untouched (ExternalPadExportFetcher keeps its own public-IP / DNS-rebinding guards).
  • Tests: the injected client is finally mockable. New unit tests cover success, apikey sourced from AdminSettingsRepository::getApiKey() (the Store the Etherpad API key as a sensitive app config value #105 regression guard, previously only catchable via e2e), non-zero API code, invalid JSON, the HTTP-error cause chain, and transport-failure wrapping. Added OCP\Http\Client stubs (IClientService/IClient/IResponse) for PHPUnit.

Acceptance

  • No file_get_contents for HTTP in EtherpadClient
  • Redirects stay disabled
  • New tests cover error/status paths via a mocked client
  • EtherpadClientTest constructor calls updated

Verification

  • PHPUnit 402 green; Psalm green (baseline shrank by 1 — the refactor removed a RedundantCast).
  • Deployed to NC 33; full Playwright suite 23 passed, 0 flaky — pad create/open (createGroup/createAuthor/createSession) and the admin connection test all work over the new client.

Note: caught a real regression mid-PR (local-address blocking) that PHPUnit could not see — exactly the failure mode the full-e2e-before-merge rule exists for.

Closes #103.

Jaggob added 2 commits June 5, 2026 01:36
EtherpadClient talked to Etherpad via file_get_contents + stream contexts,
bypassing Nextcloud's HTTP client (instance proxy / central TLS + timeout
config). Inject OCP\Http\Client\IClientService and route both the API call
path (sendRequest) and the version-detection GET (sendPublicGetRequest)
through it.

- Redirects stay disabled (allow_redirects => ['max' => 0]) so a redirect can
  never leak the apikey to a foreign host; timeout stays 15s; Accept: json.
- NC's client throws on >= 400, so doRequest() recovers the real response via
  getResponseFromThrowable() (NC 29+; we target 31+) to keep status handling
  at the call site, and treats a response-less throwable as a transport error.
- The injected client is finally mockable: new unit tests cover success,
  apikey-from-AdminSettingsRepository (the #105 regression guard, previously
  only catchable via e2e), non-zero API code, invalid JSON, HTTP-error cause
  chain, and transport-failure wrapping.
- Add OCP Http\Client stubs (IClientService/IClient/IResponse) for PHPUnit;
  Psalm resolves the real types via nextcloud/ocp (baseline shrinks by 1).

PHPUnit 402 green; Psalm green.
Nextcloud's HTTP client blocks loopback/LAN targets by default (SSRF
protection for user-supplied URLs). The Etherpad API host is admin-configured
and is very commonly a local address (here http://localhost:9001), so the
IClientService switch made every API call fail with a LocalServerException
-> 'Etherpad API request failed: createAuthorIfNotExistsFor' (HTTP 400 on
open-by-id) -- the same surface symptom as the #105 regression.

file_get_contents had no such guard, hence this only appeared after the
IClientService refactor. Pass nextcloud.allow_local_address=true for these
trusted admin-only calls. User-reachable external pad fetching is unaffected
(ExternalPadExportFetcher keeps its own public-IP / DNS-rebinding guards).

Verified on NC 33: pad open + admin connection test work again.

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 refactors EtherpadClient to use Nextcloud’s HTTP client (OCP\Http\Client\IClientService) instead of file_get_contents/stream contexts, improving proxy/TLS/timeout integration and enabling proper unit testing of request/response handling.

Changes:

  • Inject IClientService into EtherpadClient and route both API calls and public GETs through request(), with shared request options (timeout, JSON Accept, redirects disabled, and local-address allowance).
  • Add unit tests covering common success/error paths using a mocked HTTP client.
  • Add PHPUnit stubs for OCP\Http\Client interfaces and wire them into the PHPUnit bootstrap; shrink the Psalm baseline accordingly.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
lib/Service/EtherpadClient.php Replaces file_get_contents HTTP with Nextcloud IClientService, centralizing request options and error recovery.
tests/phpunit/unit/EtherpadClientTest.php Adds unit coverage for API success, API-level errors, invalid JSON, and transport/HTTP failure wrapping.
tests/phpunit/stubs/OCP/Http/Client/IClient.php Adds PHPUnit stub for OCP\Http\Client\IClient to allow mocking without a full Nextcloud runtime.
tests/phpunit/stubs/OCP/Http/Client/IClientService.php Adds PHPUnit stub for IClientService used for dependency injection in tests.
tests/phpunit/stubs/OCP/Http/Client/IResponse.php Adds PHPUnit stub for IResponse used by the new client-based request path.
tests/phpunit/bootstrap.php Registers the new OCP HTTP client stubs for the unit test environment.
psalm-baseline.xml Removes a baseline entry made obsolete by the refactor.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

try {
return $client->getResponseFromThrowable($e);
} catch (\Throwable) {
throw new EtherpadClientException('Etherpad transport error: ' . $e->getMessage());
Comment on lines +106 to +112
public function testApiCallSurfacesHttpErrorStatusAsCause(): void {
// e.g. a wrong apikey makes Etherpad answer 401 — the failure mode the
// #105 regression produced. apiCall() wraps every sendRequest failure
// as "request failed: <method>" and preserves the HTTP error as the
// cause (exactly what the server log showed during #105).
$client = $this->clientWithResponse($this->response(401, 'Unauthorized'));

…eck comment

Review follow-ups on #103 (both non-blocking):
- Add a unit test asserting detectApiVersion() issues a plain GET against
  <host>/api with no request body and the shared base options (redirects
  disabled, allow_local_address).
- The HealthCheck transport-error branch still mentioned file_get_contents/curl;
  it now goes through IClientService, so update the comment.

Test/comment only, no runtime behaviour change. PHPUnit 403 + Psalm green.
@Jaggob
Jaggob merged commit b4f2282 into main Jun 5, 2026
13 checks passed
@Jaggob
Jaggob deleted the refactor/103-iclientservice branch June 17, 2026 14:35
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.

EtherpadClient: use the Nextcloud HTTP client instead of file_get_contents

2 participants