Skip to content

Commit 4a3b561

Browse files
authored
Merge pull request #12 from RedberryProducts/page-size-and-error-handling
Page size and error handling
2 parents ad4a3db + 84c8afa commit 4a3b561

22 files changed

+1805
-71
lines changed

README.md

Lines changed: 184 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ Don't forget to star the repo ⭐
2929
- [Configuration](#configuration)
3030
- [Features](#features)
3131
- [Usage](#usage)
32+
- [Page Size & Pagination](#page-size--pagination)
33+
- [Error Handling](#error-handling)
3234
- [Page and Database Objects API](#page-and-database-objects-api)
3335
- [Customization](#customization)
3436
- [Testing](#testing)
@@ -114,7 +116,9 @@ To get your Notion API key:
114116
🎨 **Customizable Templates** - Use Blade templates for markdown output
115117
🧩 **Custom Adapters** - Extend block adapters for specialized content
116118
**Laravel Integration** - Seamless service provider and facade support
117-
🛠️ **Configurable** - Easy configuration via Laravel config files
119+
🛠️ **Configurable** - Easy configuration via Laravel config files
120+
📊 **Pagination Support** - Automatic pagination for large pages (100+ blocks)
121+
🚨 **Error Handling** - Typed exceptions for Notion API errors
118122

119123
## Usage
120124

@@ -221,6 +225,185 @@ $page = MdNotion::make($pageId)
221225
// Access: $page->getTitle(), $page->getContent(), $page->getChildPages(), etc.
222226
```
223227

228+
## Page Size & Pagination
229+
230+
The Notion API limits responses to 100 blocks per request. This package handles pagination automatically, allowing you to fetch more blocks seamlessly.
231+
232+
### Configuration
233+
234+
Set the default page size in your `.env` file:
235+
236+
```env
237+
NOTION_DEFAULT_PAGE_SIZE=100
238+
```
239+
240+
Or in the config file:
241+
242+
```php
243+
// config/md-notion.php
244+
return [
245+
'default_page_size' => env('NOTION_DEFAULT_PAGE_SIZE', 100),
246+
// ...
247+
];
248+
```
249+
250+
### Custom Page Size
251+
252+
You can override the default page size per request:
253+
254+
```php
255+
use Redberry\MdNotion\Facades\MdNotion;
256+
257+
// Fetch up to 50 blocks
258+
$content = MdNotion::make($pageId)->content()->read(50);
259+
260+
// Fetch up to 200 blocks (automatically paginated)
261+
$content = MdNotion::make($pageId)->content()->read(200);
262+
263+
// Use default from config
264+
$content = MdNotion::make($pageId)->content()->read();
265+
```
266+
267+
### How Pagination Works
268+
269+
- **Page size ≤ 100**: Single API request
270+
- **Page size > 100**: Automatic pagination with multiple requests
271+
272+
The returned data always has a consistent structure:
273+
274+
```php
275+
[
276+
'results' => [...], // Array of blocks
277+
'has_more' => bool, // Whether more items exist
278+
'next_cursor' => ?string // Cursor for manual continuation (null if results were trimmed)
279+
]
280+
```
281+
282+
> **Note**: When results are trimmed to meet your requested limit, `next_cursor` is set to `null` to prevent accidentally skipping items. The `has_more` flag will still indicate if more items exist.
283+
284+
### Validation
285+
286+
Page size must be a positive integer. Invalid values will throw an exception:
287+
288+
```php
289+
// These will throw InvalidArgumentException:
290+
MdNotion::make($pageId)->content()->read(0); // Zero not allowed
291+
MdNotion::make($pageId)->content()->read(-5); // Negative not allowed
292+
```
293+
294+
## Error Handling
295+
296+
The package provides a dedicated `NotionApiException` for handling Notion API errors with detailed information.
297+
298+
### Basic Error Handling
299+
300+
```php
301+
use Redberry\MdNotion\Facades\MdNotion;
302+
use Redberry\MdNotion\SDK\Exceptions\NotionApiException;
303+
304+
try {
305+
$content = MdNotion::make($pageId)->content()->read();
306+
} catch (NotionApiException $e) {
307+
// Get error details
308+
echo $e->getMessage(); // "Notion API Error [404] object_not_found: Could not find page..."
309+
echo $e->getNotionCode(); // "object_not_found"
310+
echo $e->getNotionMessage(); // "Could not find page with ID: ..."
311+
312+
// Access the original response
313+
$response = $e->getResponse();
314+
$statusCode = $response->status(); // 404
315+
}
316+
```
317+
318+
### Error Type Checking
319+
320+
The exception provides convenient methods to check error types:
321+
322+
```php
323+
try {
324+
$content = MdNotion::make($pageId)->content()->read();
325+
} catch (NotionApiException $e) {
326+
if ($e->isNotFound()) {
327+
// Page doesn't exist or not shared with integration
328+
}
329+
330+
if ($e->isUnauthorized()) {
331+
// Invalid API key
332+
}
333+
334+
if ($e->isForbidden()) {
335+
// Integration doesn't have access to this resource
336+
}
337+
338+
if ($e->isRateLimited()) {
339+
// Too many requests, implement backoff
340+
}
341+
342+
if ($e->isValidationError()) {
343+
// Invalid request parameters
344+
}
345+
346+
if ($e->isServerError()) {
347+
// Notion server error (5xx)
348+
}
349+
350+
if ($e->isRetryable()) {
351+
// Safe to retry (rate limits, server errors, conflicts)
352+
}
353+
}
354+
```
355+
356+
### Notion Error Codes
357+
358+
The `getNotionCode()` method returns one of these values:
359+
360+
| Code | HTTP Status | Description |
361+
|------|-------------|-------------|
362+
| `invalid_json` | 400 | Request body is not valid JSON |
363+
| `invalid_request_url` | 400 | Invalid request URL |
364+
| `invalid_request` | 400 | Invalid request parameters |
365+
| `validation_error` | 400 | Request validation failed |
366+
| `missing_version` | 400 | Missing Notion-Version header |
367+
| `unauthorized` | 401 | Invalid API key |
368+
| `restricted_resource` | 403 | No access to resource |
369+
| `object_not_found` | 404 | Resource not found |
370+
| `conflict_error` | 409 | Transaction conflict |
371+
| `rate_limited` | 429 | Too many requests |
372+
| `internal_server_error` | 500 | Notion server error |
373+
| `bad_gateway` | 502 | Bad gateway |
374+
| `service_unavailable` | 503 | Service temporarily unavailable |
375+
| `gateway_timeout` | 504 | Gateway timeout |
376+
377+
### Retry Strategy Example
378+
379+
```php
380+
use Redberry\MdNotion\SDK\Exceptions\NotionApiException;
381+
382+
function fetchWithRetry(string $pageId, int $maxRetries = 3): string
383+
{
384+
$attempts = 0;
385+
386+
while ($attempts < $maxRetries) {
387+
try {
388+
return MdNotion::make($pageId)->content()->read();
389+
} catch (NotionApiException $e) {
390+
if (!$e->isRetryable()) {
391+
throw $e; // Don't retry non-retryable errors
392+
}
393+
394+
$attempts++;
395+
if ($attempts >= $maxRetries) {
396+
throw $e;
397+
}
398+
399+
// Exponential backoff
400+
$delay = $e->isRateLimited() ? 1000 : 500;
401+
usleep($delay * $attempts * 1000);
402+
}
403+
}
404+
}
405+
```
406+
224407
## Page and Database Objects API
225408

226409
The `MdNotion` package provides rich object models for working with Notion pages and databases. Both `Page` and `Database` objects extend `BaseObject` and use several traits to provide comprehensive functionality.

examples/error-handling-test.php

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?php
2+
3+
/**
4+
* Manual Test: Notion API Error Handling
5+
*
6+
* This script demonstrates how the SDK now throws NotionApiException
7+
* when the Notion API returns an error response.
8+
*/
9+
10+
require_once __DIR__.'/../vendor/autoload.php';
11+
12+
use Redberry\MdNotion\SDK\Exceptions\NotionApiException;
13+
use Redberry\MdNotion\SDK\Notion;
14+
15+
echo "=== Manual Test: Notion API Error Handling ===\n\n";
16+
17+
// Test 1: Invalid Token
18+
echo "Test 1: Invalid Token (401 Unauthorized)\n";
19+
echo str_repeat('-', 50)."\n";
20+
21+
try {
22+
$notion = new Notion('invalid-token', '2025-09-03');
23+
$notion->act()->getPage('some-page-id');
24+
echo "❌ Expected exception was not thrown\n";
25+
} catch (NotionApiException $e) {
26+
echo "✅ NotionApiException caught!\n";
27+
echo ' Status: '.$e->getResponse()->status()."\n";
28+
echo ' Code: '.$e->getNotionCode()."\n";
29+
echo ' Message: '.$e->getNotionMessage()."\n";
30+
echo ' isUnauthorized(): '.($e->isUnauthorized() ? 'true' : 'false')."\n";
31+
echo ' isRetryable(): '.($e->isRetryable() ? 'true' : 'false')."\n";
32+
} catch (Exception $e) {
33+
echo '❌ Unexpected exception: '.get_class($e)."\n";
34+
echo ' Message: '.$e->getMessage()."\n";
35+
}
36+
37+
echo "\n";
38+
39+
// Test 2: Object Not Found (valid token, non-existent page)
40+
echo "Test 2: Object Not Found (404)\n";
41+
echo str_repeat('-', 50)."\n";
42+
43+
// Load real token if available
44+
$tokenFile = __DIR__.'/../notion-token.php';
45+
if (file_exists($tokenFile)) {
46+
$token = include $tokenFile;
47+
48+
try {
49+
$notion = new Notion($token, '2025-09-03');
50+
// Try to fetch a non-existent page
51+
$notion->act()->getPage('00000000-0000-0000-0000-000000000000');
52+
echo "❌ Expected exception was not thrown\n";
53+
} catch (NotionApiException $e) {
54+
echo "✅ NotionApiException caught!\n";
55+
echo ' Status: '.$e->getResponse()->status()."\n";
56+
echo ' Code: '.$e->getNotionCode()."\n";
57+
echo ' Message: '.$e->getNotionMessage()."\n";
58+
echo ' isNotFound(): '.($e->isNotFound() ? 'true' : 'false')."\n";
59+
echo ' isRetryable(): '.($e->isRetryable() ? 'true' : 'false')."\n";
60+
} catch (Exception $e) {
61+
echo '❌ Unexpected exception: '.get_class($e)."\n";
62+
echo ' Message: '.$e->getMessage()."\n";
63+
}
64+
} else {
65+
echo "⏭️ Skipped (no notion-token.php found)\n";
66+
}
67+
68+
echo "\n";
69+
70+
// Test 3: Successful Request (for comparison)
71+
echo "Test 3: Successful Request\n";
72+
echo str_repeat('-', 50)."\n";
73+
74+
if (file_exists($tokenFile)) {
75+
$token = include $tokenFile;
76+
77+
try {
78+
$notion = new Notion($token, '2025-09-03');
79+
// Use a known valid page ID from your workspace
80+
$response = $notion->act()->getPage('24cd937adaa8811c8dd5c2a5ed7eb453');
81+
echo "✅ Request successful!\n";
82+
echo ' Status: '.$response->status()."\n";
83+
echo ' Page ID: '.$response->json()['id']."\n";
84+
} catch (NotionApiException $e) {
85+
echo '❌ NotionApiException: '.$e->getMessage()."\n";
86+
echo " You may need to update the page ID to one accessible by your integration.\n";
87+
} catch (Exception $e) {
88+
echo '❌ Unexpected exception: '.get_class($e)."\n";
89+
echo ' Message: '.$e->getMessage()."\n";
90+
}
91+
} else {
92+
echo "⏭️ Skipped (no notion-token.php found)\n";
93+
}
94+
95+
echo "\n=== Error Handling Summary ===\n\n";
96+
echo "The SDK now throws NotionApiException for all API errors.\n";
97+
echo "Available helper methods:\n";
98+
echo " - getNotionCode() - Get the error code (e.g., 'unauthorized', 'object_not_found')\n";
99+
echo " - getNotionMessage() - Get the human-readable error message\n";
100+
echo " - isUnauthorized() - Check if 401 unauthorized\n";
101+
echo " - isForbidden() - Check if 403 restricted_resource\n";
102+
echo " - isNotFound() - Check if 404 object_not_found\n";
103+
echo " - isRateLimited() - Check if 429 rate_limited\n";
104+
echo " - isValidationError() - Check if 400 validation_error\n";
105+
echo " - isServerError() - Check if 5xx server error\n";
106+
echo " - isRetryable() - Check if error is retryable (rate limits, server errors, conflicts)\n";
107+
echo " - getResponse() - Access the full Saloon Response object\n";

0 commit comments

Comments
 (0)