Skip to content
Open
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
36d5d5e
feat: create Future class
Art4 Mar 10, 2026
967b977
feat: create Future::enableForwardCompatibility() method
Art4 Mar 10, 2026
9351bb8
feat: create Future::disableForwardCompatibility() method
Art4 Mar 10, 2026
1d89ab0
feat: implement enableFutureMode() in Client implementations
Art4 Mar 10, 2026
dd287a2
feat: check for correct response status code on create issue
Art4 Mar 10, 2026
c06b62b
feat: check for correct response status code on Attachment::upload()
Art4 Jul 26, 2026
fb7dbfc
feat: check for correct response status code on Attachment::remove()
Art4 Jul 26, 2026
93d7c12
feat: check for correct response status code on IssueCategory::create()
Art4 Jul 26, 2026
cd32963
feat: check for correct response status code on IssueCategory::update()
Art4 Jul 26, 2026
0c595c1
feat: check for correct response status code on IssueCategory::remove()
Art4 Jul 26, 2026
0ca530c
feat: check for correct response status code on Membership::create()
Art4 Jul 26, 2026
5bc2265
feat: check for correct response status code on Membership::update()
Art4 Jul 26, 2026
266a679
feat: check for correct response status code on Membership::remove()
Art4 Jul 26, 2026
b3f8ea2
feat: check for correct response status code on TimeEntry::create()
Art4 Jul 26, 2026
1d87d20
feat: check for correct response status code on TimeEntry::update()
Art4 Jul 26, 2026
e02ef15
feat: check for correct response status code on TimeEntry::remove()
Art4 Jul 26, 2026
e7a8594
feat: check for correct response status code on Version::create()
Art4 Jul 26, 2026
b12eb35
feat: check for correct response status code on Version::update()
Art4 Jul 26, 2026
a8b916f
feat: check for correct response status code on Version::remove()
Art4 Jul 26, 2026
85df940
feat: check for correct response status code on Group::create()
Art4 Jul 26, 2026
9a28ecb
feat: check for correct response status code on Group::update()
Art4 Jul 26, 2026
8ce91bb
feat: check for correct response status code on Group::remove()
Art4 Jul 26, 2026
da6eb44
feat: check for correct response status code on Group::addUser()
Art4 Jul 26, 2026
12be2de
feat: check for correct response status code on Group::removeUser()
Art4 Jul 26, 2026
f4aa6d2
feat: check for correct response status code on User::create()
Art4 Jul 26, 2026
3e17bf5
feat: check for correct response status code on User::update()
Art4 Jul 26, 2026
3377864
feat: check for correct response status code on User::remove()
Art4 Jul 26, 2026
40d9d97
feat: check for correct response status code on Project::create()
Art4 Jul 26, 2026
1e14c25
feat: check for correct response status code on Project::update()
Art4 Jul 26, 2026
c8710ca
feat: check for correct response status code on Project::remove()
Art4 Jul 26, 2026
59cd660
feat: check for correct response status code on IssueRelation::create()
Art4 Jul 26, 2026
2dbbda2
feat: check for correct response status code on IssueRelation::remove()
Art4 Jul 26, 2026
ee31f23
feat: check for correct response status code on Issue::update()
Art4 Jul 26, 2026
33877e7
feat: check for correct response status code on Issue::remove()
Art4 Jul 26, 2026
cf51454
feat: check for correct response status code on Issue::addWatcher()
Art4 Jul 26, 2026
42bb0fe
feat: check for correct response status code on Issue::removeWatcher()
Art4 Jul 26, 2026
26f14c0
feat: check for correct response status code on Issue::attachMany()
Art4 Jul 26, 2026
87faab7
feat: check for correct response status code on Wiki::create()
Art4 Jul 26, 2026
48f500e
feat: check for correct response status code on Wiki::remove()
Art4 Jul 26, 2026
5f68c45
fix: correct BC guard in Issue::create(), IssueCategory::create(), Me…
Art4 Jul 26, 2026
044cb8a
docs: add enableFutureMode() documentation, update changelog, remove …
Art4 Jul 26, 2026
298d432
test: use non-empty response body in incorrect-status tests for remov…
Art4 Jul 26, 2026
c4ce4ac
test: wrap enableFutureMode tests in try/finally to always restore gl…
Art4 Jul 26, 2026
9b4b876
test: add non-empty body BC tests for create methods, fix FutureTest …
Art4 Jul 26, 2026
7c2012a
feat: return SimpleXMLElement from BC guard when body is non-empty XML
Art4 Jul 26, 2026
fe7faca
test: add non-empty body BC coverage for Group::addUser and Issue::ad…
Art4 Jul 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Add support for Redmine 7.0.
- `\Redmine\Client\NativeCurlClient::enableFutureMode()` to opt-in to throwing `UnexpectedResponseException` on unexpected status codes.
- `\Redmine\Client\Psr18Client::enableFutureMode()` to opt-in to throwing `UnexpectedResponseException` on unexpected status codes.
- API methods that create, update, or delete resources now verify the HTTP response status code. The return value only changes if `enableFutureMode()` was called – unexpected status codes then throw `UnexpectedResponseException` instead of returning the response body.

### Changed

Expand Down
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,6 @@ you are welcome to [create an issue](https://github.com/kbsali/php-redmine-api/i
## Todo

* Tracking of Redmine API feature support in [#305](https://github.com/kbsali/php-redmine-api/issues/305)
* Check header's response code (especially for POST/PUT/DELETE requests)
* See https://stackoverflow.com/questions/9183178/php-curl-retrieving-response-headers-and-body-in-a-single-request/9183272#9183272

## Limitations / Missing Redmine-API

Expand Down
28 changes: 28 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,34 @@ try {
}
```

### Forward compatibility mode

By default, API methods that create, update, or delete resources return the raw response body even if the HTTP status code indicates an error. This preserves backwards compatibility with existing code.

You can opt-in to stricter behavior by enabling the forward compatibility mode on the client:

```php
$client->enableFutureMode();
```

After calling `enableFutureMode()`, any unexpected status code (e.g. a 422 validation error) throws `\Redmine\Exception\UnexpectedResponseException` instead of returning the response body. This allows you to catch and handle errors more reliably.

```php
$client->enableFutureMode();

try {
$client->getApi('issue')->create([
'project_id' => 1,
'subject' => 'test',
]);
} catch (\Redmine\Exception\UnexpectedResponseException $e) {
// Handle unexpected response (wrong status code)
$response = $e->getResponse();
$statusCode = $response->getStatusCode();
$body = $response->getContent();
}
```

## API

### Mid-level API
Expand Down
26 changes: 24 additions & 2 deletions src/Redmine/Api/Attachment.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Redmine\Client\Client;
use Redmine\Exception\SerializerException;
use Redmine\Exception\UnexpectedResponseException;
use Redmine\Future;
use Redmine\Http\HttpClient;
use Redmine\Http\HttpFactory;
use Redmine\Serializer\JsonSerializer;
Expand Down Expand Up @@ -156,7 +157,17 @@ public function upload($attachment, $params = [])
$attachment,
));

return $this->lastResponse->getContent();
$body = $this->lastResponse->getContent();

if ($this->lastResponse->getStatusCode() !== 201) {
if (!Future::isForwardCompatibilityEnabled()) {
return $body;
}

throw UnexpectedResponseException::create($this->lastResponse);
}

return $body;
}

/**
Expand All @@ -175,6 +186,17 @@ public function remove($id)
'/attachments/' . urlencode(strval($id)) . '.xml',
));

return $this->lastResponse->getContent();
$body = $this->lastResponse->getContent();
$statusCode = $this->lastResponse->getStatusCode();

if ($statusCode !== 200 && $statusCode !== 204) {
if (!Future::isForwardCompatibilityEnabled()) {
return $body;
}

throw UnexpectedResponseException::create($this->lastResponse);
}

return $body;
}
}
58 changes: 55 additions & 3 deletions src/Redmine/Api/Group.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Redmine\Exception\MissingParameterException;
use Redmine\Exception\SerializerException;
use Redmine\Exception\UnexpectedResponseException;
use Redmine\Future;
use Redmine\Http\HttpClient;
use Redmine\Http\HttpFactory;
use Redmine\Serializer\JsonSerializer;
Expand Down Expand Up @@ -195,6 +196,15 @@ public function create(array $params = [])
));

$body = $this->lastResponse->getContent();
$statusCode = $this->lastResponse->getStatusCode();

if ($statusCode !== 201) {
if (!Future::isForwardCompatibilityEnabled()) {
return $body;
}

throw UnexpectedResponseException::create($this->lastResponse);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if ($body === '') {
return $body;
Expand Down Expand Up @@ -229,7 +239,18 @@ public function update(int $id, array $params = [])
XmlSerializer::createFromArray(['group' => $params])->getEncoded(),
));

return $this->lastResponse->getContent();
$body = $this->lastResponse->getContent();
$statusCode = $this->lastResponse->getStatusCode();

if ($statusCode !== 200 && $statusCode !== 204) {
if (!Future::isForwardCompatibilityEnabled()) {
return $body;
}

throw UnexpectedResponseException::create($this->lastResponse);
}

return $body;
}

/**
Expand Down Expand Up @@ -280,7 +301,18 @@ public function remove($id)
'/groups/' . strval($id) . '.xml',
));

return $this->lastResponse->getContent();
$body = $this->lastResponse->getContent();
$statusCode = $this->lastResponse->getStatusCode();

if ($statusCode !== 200 && $statusCode !== 204) {
if (!Future::isForwardCompatibilityEnabled()) {
return $body;
}

throw UnexpectedResponseException::create($this->lastResponse);
}

return $body;
}

/**
Expand All @@ -302,6 +334,15 @@ public function addUser($id, $userId)
));

$body = $this->lastResponse->getContent();
$statusCode = $this->lastResponse->getStatusCode();

if ($statusCode !== 201) {
if (!Future::isForwardCompatibilityEnabled()) {
return $body;
}

throw UnexpectedResponseException::create($this->lastResponse);
}

if ($body === '') {
return $body;
Expand All @@ -327,6 +368,17 @@ public function removeUser($id, $userId)
'/groups/' . strval($id) . '/users/' . strval($userId) . '.xml',
));

return $this->lastResponse->getContent();
$body = $this->lastResponse->getContent();
$statusCode = $this->lastResponse->getStatusCode();

if ($statusCode !== 200 && $statusCode !== 204) {
if (!Future::isForwardCompatibilityEnabled()) {
return $body;
}

throw UnexpectedResponseException::create($this->lastResponse);
}

return $body;
}
}
74 changes: 68 additions & 6 deletions src/Redmine/Api/Issue.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Redmine\Exception;
use Redmine\Exception\SerializerException;
use Redmine\Exception\UnexpectedResponseException;
use Redmine\Future;
use Redmine\Http\HttpClient;
use Redmine\Http\HttpFactory;
use Redmine\Serializer\JsonSerializer;
Expand Down Expand Up @@ -256,8 +257,16 @@ public function create(array $params = [])

$body = $this->lastResponse->getContent();

if ($body === '') {
return $body;
if ($this->lastResponse->getStatusCode() !== 201) {
if (!Future::isForwardCompatibilityEnabled()) {
if ($body === '') {
return $body;
}

return new SimpleXMLElement($body);
}

throw UnexpectedResponseException::create($this->lastResponse);
}

return new SimpleXMLElement($body);
Expand Down Expand Up @@ -306,7 +315,18 @@ public function update($id, array $params)
XmlSerializer::createFromArray(['issue' => $sanitizedParams])->getEncoded(),
));

return $this->lastResponse->getContent();
$body = $this->lastResponse->getContent();
$statusCode = $this->lastResponse->getStatusCode();

if ($statusCode !== 200 && $statusCode !== 204) {
if (!Future::isForwardCompatibilityEnabled()) {
return $body;
}

throw UnexpectedResponseException::create($this->lastResponse);
}

return $body;
}

/**
Expand All @@ -324,6 +344,15 @@ public function addWatcher($id, $watcherUserId)
));

$body = $this->lastResponse->getContent();
$statusCode = $this->lastResponse->getStatusCode();

if ($statusCode !== 201) {
if (!Future::isForwardCompatibilityEnabled()) {
return $body;
}

throw UnexpectedResponseException::create($this->lastResponse);
}

if ($body === '') {
return $body;
Expand All @@ -345,7 +374,18 @@ public function removeWatcher($id, $watcherUserId)
'/issues/' . urlencode(strval($id)) . '/watchers/' . urlencode(strval($watcherUserId)) . '.xml',
));

return $this->lastResponse->getContent();
$body = $this->lastResponse->getContent();
$statusCode = $this->lastResponse->getStatusCode();

if ($statusCode !== 200 && $statusCode !== 204) {
if (!Future::isForwardCompatibilityEnabled()) {
return $body;
}

throw UnexpectedResponseException::create($this->lastResponse);
}

return $body;
}

/**
Expand Down Expand Up @@ -498,7 +538,18 @@ public function attachMany($id, array $attachments)
JsonSerializer::createFromArray(['issue' => $params])->getEncoded(),
));

return $this->lastResponse->getContent();
$body = $this->lastResponse->getContent();
$statusCode = $this->lastResponse->getStatusCode();

if ($statusCode !== 200 && $statusCode !== 201 && $statusCode !== 204) {
if (!Future::isForwardCompatibilityEnabled()) {
return $body;
}

throw UnexpectedResponseException::create($this->lastResponse);
}

return $body;
}

/**
Expand All @@ -515,7 +566,18 @@ public function remove($id)
'/issues/' . urlencode(strval($id)) . '.xml',
));

return $this->lastResponse->getContent();
$body = $this->lastResponse->getContent();
$statusCode = $this->lastResponse->getStatusCode();

if ($statusCode !== 200 && $statusCode !== 204) {
if (!Future::isForwardCompatibilityEnabled()) {
return $body;
}

throw UnexpectedResponseException::create($this->lastResponse);
}

return $body;
}

/**
Expand Down
39 changes: 37 additions & 2 deletions src/Redmine/Api/IssueCategory.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Redmine\Exception\MissingParameterException;
use Redmine\Exception\SerializerException;
use Redmine\Exception\UnexpectedResponseException;
use Redmine\Future;
use Redmine\Http\HttpClient;
use Redmine\Http\HttpFactory;
use Redmine\Serializer\JsonSerializer;
Expand Down Expand Up @@ -267,6 +268,18 @@ public function create($project, array $params = [])

$body = $this->lastResponse->getContent();

if ($this->lastResponse->getStatusCode() !== 201) {
if (!Future::isForwardCompatibilityEnabled()) {
if ($body === '') {
return $body;
}

return new SimpleXMLElement($body);
}

throw UnexpectedResponseException::create($this->lastResponse);
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
if ($body === '') {
return $body;
}
Expand Down Expand Up @@ -298,7 +311,18 @@ public function update($id, array $params)
XmlSerializer::createFromArray(['issue_category' => $params])->getEncoded(),
));

return $this->lastResponse->getContent();
$body = $this->lastResponse->getContent();
$statusCode = $this->lastResponse->getStatusCode();

if ($statusCode !== 200 && $statusCode !== 204) {
if (!Future::isForwardCompatibilityEnabled()) {
return $body;
}

throw UnexpectedResponseException::create($this->lastResponse);
}

return $body;
}

/**
Expand All @@ -320,7 +344,18 @@ public function remove($id, array $params = [])
PathSerializer::create('/issue_categories/' . urlencode(strval($id)) . '.xml', $params)->getPath(),
));

return $this->lastResponse->getContent();
$body = $this->lastResponse->getContent();
$statusCode = $this->lastResponse->getStatusCode();

if ($statusCode !== 200 && $statusCode !== 204) {
if (!Future::isForwardCompatibilityEnabled()) {
return $body;
}

throw UnexpectedResponseException::create($this->lastResponse);
}

return $body;
}

/**
Expand Down
Loading
Loading