Repository: CS-SI/eodag
Pull request #1237 (2024-06-25 10:45:13)

feat(core): improve search and authentication errors format
Improves search and authentication errors format :
- [include status_code in request errors when possible](https://github.com/CS-SI/eodag/pull/1237/commits/d0901457eb2e5672e55f12597820f781463df0ec)
- [method from_error() to generate RequestError](https://github.com/CS-SI/eodag/pull/1237/commits/2ef829cb857bdf3fdd2be84a0a3b8db5582ac491)
- [explode exception messages to different args](https://github.com/CS-SI/eodag/pull/1237/commits/b3a73eb98afe5da32fdfb4f93b77ca3d0264c0c2)
- [bind search errors to SearchResult instead of search plugin](https://github.com/CS-SI/eodag/pull/1237/commits/28e4e8ace7c15384c5f3d04d5793686b4f5a43bc)
- [[server]: use new ResponseSearchError to handle search result…](https://github.com/CS-SI/eodag/pull/1237/commits/2ce7776dfd8a5bc5274b9830bfd9459d49048379)
- [update search logs flow](https://github.com/CS-SI/eodag/pull/1237/commits/00cf8b15e7b52c9d720ceedec38321c53c026de4)

github-actions[bot]: ## Test Results
 3 files   -     1  3 suites   - 1   1m 12s :stopwatch: - 4m 56s
57 tests  -   491  0 :white_check_mark:  -   545  0 :zzz:  -   3  0 :x: ±0  57 :fire: +57 
57 runs   - 2 135  0 :white_check_mark:  - 2 090  0 :zzz:  - 102  0 :x: ±0  57 :fire: +57 

For more details on these errors, see [this check](https://github.com/CS-SI/eodag/runs/26647960186).

Results for commit 8e36d779. ± Comparison against base commit 1e0e5770.

<details>
  <summary>This pull request <b>removes</b> 524 and <b>adds</b> 33 tests. <i>Note that renamed tests count towards both.</i></summary>

```
eodag.types.__init__ ‑ eodag.types.json_field_definition_to_python
eodag.types.__init__ ‑ eodag.types.json_type_to_python
eodag.types.__init__ ‑ eodag.types.model_fields_to_annotated
eodag.types.__init__ ‑ eodag.types.python_field_definition_to_json
eodag.types.__init__ ‑ eodag.types.python_type_to_json
tests.integration.test_config_ext_plugin.TestExternalPluginConfig ‑ test_update_providers_from_ext_plugin
tests.integration.test_core_config.TestCoreProductTypesConfig ‑ test_core_discover_product_types_auth
tests.integration.test_core_config.TestCoreProvidersConfig ‑ test_core_providers_config_update
tests.integration.test_core_search.TestCoreSearch ‑ test_core_search_errors_buildpost
tests.integration.test_core_search.TestCoreSearch ‑ test_core_search_errors_odata
…
```

```
eodag.types.__init__
eodag.utils.constraints
tests.__init__
tests.context
tests.integration.__init__
tests.integration.test_config_ext_plugin
tests.integration.test_core_config
tests.integration.test_core_search
tests.integration.test_core_search_results
tests.integration.test_download_auth
…
```
</details>

[test-results]:data:application/gzip;base64,H4sIAHegemYC/1XMSw6DIBSF4a0Yxh0oCGg30yCP5KYqDY+R6d571VhhQvjOTf6NOJhtJM+GPRoSM6Q/TA4qgV+RkqLxkvYblxdeMWuNS3sPb/hUg1MwV4MNwYerEvIay38ZPH33Dhe5w1VN+2WBhCKDZcJIOQqph27kxuLTT04zQxETp71QrjUd+f4AqIA0zQABAAA=

github-actions[bot]: ![badge](https://raw.githubusercontent.com/CS-SI/eodag/_xml_coverage_reports/data/wip-server-errors/./badge.svg)


## Code Coverage (Ubuntu)

<details>


```
Filename                                     Stmts    Miss  Cover    Missing
-----------------------------------------  -------  ------  -------  -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
__init__.py                                     10       0  100.00%
cli.py                                         305      51  83.28%   60, 654-700, 802-853, 857
config.py                                      351      27  92.31%   81-83, 92, 100, 104-106, 183, 195, 460-462, 526-529, 576-577, 586-587, 666, 735-740, 742
crunch.py                                        5       5  0.00%    20-24
api/__init__.py                                  0       0  100.00%
api/core.py                                    753      83  88.98%   90-99, 371, 596, 640-643, 681, 718, 791, 795-800, 826, 922, 1005, 1142, 1234-1246, 1286, 1288, 1316, 1320-1331, 1344-1350, 1440-1443, 1472-1492, 1548, 1565-1568, 1580-1583, 1601-1608, 1932, 1958-1964, 2231, 2235-2238, 2252-2254, 2289
api/search_result.py                            59       6  89.83%   35-37, 85, 94, 101, 115
api/product/__init__.py                          5       0  100.00%
api/product/_assets.py                          44       6  86.36%   27-29, 79, 119, 129
api/product/_product.py                        188      27  85.64%   56-63, 67-69, 239-240, 322, 351, 412, 426-429, 442, 466-469, 512-518
api/product/metadata_mapping.py                678      83  87.76%   67-69, 131-133, 234, 266-267, 313-314, 324-336, 338, 349, 355-367, 412-413, 450, 471-474, 497, 505-506, 582-583, 607-608, 614-617, 632-633, 782, 828, 1002-1007, 1138, 1152-1172, 1192, 1197, 1307, 1329, 1343, 1356-1375, 1414, 1466, 1504-1508, 1527
api/product/drivers/__init__.py                  5       0  100.00%
api/product/drivers/base.py                      8       2  75.00%   23, 41
plugins/__init__.py                              0       0  100.00%
plugins/base.py                                 23       3  86.96%   25, 48, 55
plugins/manager.py                             133      15  88.72%   49-51, 105-110, 156, 197, 219, 223, 248, 287-288
plugins/apis/__init__.py                         0       0  100.00%
plugins/apis/base.py                             4       0  100.00%
plugins/apis/ecmwf.py                           99      15  84.85%   46-54, 150-152, 199-200, 226-228
plugins/apis/usgs.py                           175      36  79.43%   60-65, 132, 205, 239, 274-276, 281, 307-308, 313, 343-350, 361-366, 388-394, 396-402, 425
plugins/authentication/__init__.py               6       1  83.33%   31
plugins/authentication/aws_auth.py              20       2  90.00%   25-27
plugins/authentication/base.py                  19       3  84.21%   26, 34, 47
plugins/authentication/generic.py               16       3  81.25%   28, 40, 50
plugins/authentication/header.py                21       1  95.24%   28
plugins/authentication/keycloak.py              49       6  87.76%   33-35, 132, 156-161
plugins/authentication/oauth.py                 15       8  46.67%   25, 32-34, 38-41
plugins/authentication/openid_connect.py       185      19  89.73%   36-38, 114, 128-153, 161, 315-318, 342
plugins/authentication/qsauth.py                36       2  94.44%   32, 83
plugins/authentication/sas_auth.py              49       2  95.92%   32, 76
plugins/authentication/token.py                 90      17  81.11%   35-37, 79, 108, 110, 132-144, 199-202
plugins/authentication/token_exchange.py        35      19  45.71%   74-80, 92-120
plugins/crunch/__init__.py                       0       0  100.00%
plugins/crunch/base.py                          10       2  80.00%   25, 38
plugins/crunch/filter_date.py                   62      15  75.81%   30, 53-58, 72, 81, 90, 93, 105-107, 116-118, 125
plugins/crunch/filter_latest_intersect.py       50      10  80.00%   32-34, 51-52, 71, 80-83, 85, 92-95
plugins/crunch/filter_latest_tpl_name.py        33       2  93.94%   28, 86
plugins/crunch/filter_overlap.py                68      19  72.06%   28-30, 33, 72-75, 82-85, 91, 99, 110-126
plugins/crunch/filter_property.py               33       8  75.76%   29, 60-65, 68-69, 85-89
plugins/download/__init__.py                     0       0  100.00%
plugins/download/aws.py                        492     165  66.46%   77-83, 272, 285, 352-355, 369-373, 419-421, 425, 459-460, 466-470, 503, 538, 542, 549, 579-587, 591, 629-637, 644-646, 687-761, 779-840, 851-856, 872-885, 914, 929-931, 934, 944-952, 960-973, 983-1002, 1009-1021, 1062, 1088, 1133-1135, 1355
plugins/download/base.py                       260      57  78.08%   58-64, 145, 180, 319-320, 340-346, 377-381, 387-388, 432, 435-449, 461, 465, 538-542, 572-573, 581-598, 605-613, 615-619, 666, 688, 710, 718
plugins/download/creodias_s3.py                 17       9  47.06%   44-58
plugins/download/http.py                       531     132  75.14%   85-91, 197-209, 211-212, 247-250, 314-317, 319-320, 327-332, 350-365, 382-384, 396, 444, 451-457, 475, 489, 503, 511-513, 529-534, 545, 563, 605-609, 631, 671, 716, 730-736, 772-836, 854, 887-896, 922-923, 950-955, 961, 964, 980, 997-998, 1025-1026, 1033, 1095-1101, 1156-1157, 1163, 1173, 1207, 1243, 1261-1277
plugins/download/s3rest.py                     118      27  77.12%   55-58, 121, 162, 197, 227-234, 237-239, 243, 256-262, 270-271, 274-278, 301, 322-325
plugins/search/__init__.py                      26       3  88.46%   27-31
plugins/search/base.py                         133      13  90.23%   48-53, 104, 108, 121, 271, 291, 350-351, 371, 380
plugins/search/build_search_result.py          183      24  86.89%   62, 97, 141-142, 148, 159, 295-298, 327, 384-401, 463, 466, 476, 493, 521, 523
plugins/search/cop_marine.py                   200      49  75.50%   43-44, 55, 63-65, 71-72, 88, 90, 93, 128-130, 142-143, 183-192, 196, 199, 203, 221, 251, 255, 270, 274, 278, 282, 286-290, 296-299, 302-316, 333, 356, 359, 365
plugins/search/creodias_s3.py                   55       3  94.55%   61, 79, 113
plugins/search/csw.py                          108      83  23.15%   44-46, 58-59, 63-64, 72-120, 126-139, 147-179, 197-238
plugins/search/data_request_search.py          200      67  66.50%   53, 90-93, 109, 121, 125-126, 137, 142, 147, 154, 167-170, 224-225, 229, 239-245, 250, 276-279, 287-298, 315, 317, 324-327, 329-330, 348-352, 385, 392, 403, 416, 422-434, 439
plugins/search/qssearch.py                     645      69  89.30%   102, 386, 390-396, 404-405, 511-517, 567, 583, 593, 620, 622, 665-668, 742-743, 789, 808, 823, 881, 902, 905-906, 915, 926, 935, 958, 1029-1034, 1038-1039, 1072, 1146, 1195, 1269-1273, 1339-1340, 1361, 1388-1400, 1407, 1439-1441, 1451-1457, 1501, 1516, 1534, 1633-1643
plugins/search/static_stac_search.py            73      12  83.56%   37-38, 95-122, 138, 151
rest/__init__.py                                 4       2  50.00%   21-22
rest/cache.py                                   33       7  78.79%   35-37, 53-55, 59, 68
rest/config.py                                  26       0  100.00%
rest/constants.py                                7       0  100.00%
rest/core.py                                   240      31  87.08%   84-88, 227, 285, 369, 479, 706, 713-761
rest/errors.py                                  31      18  41.94%   51, 56-80, 85-88, 95
rest/server.py                                 275      42  84.73%   82-83, 109, 132-134, 250-252, 308-309, 321-322, 414-419, 447, 615-622, 651, 695-696, 719, 792-794, 811-816, 845, 847, 851-852, 856-857
rest/stac.py                                   466     123  73.61%   65-67, 320, 342, 390-393, 417-443, 448-454, 477-479, 502, 537-538, 566, 579, 624-664, 692-708, 787-800, 807, 863-864, 931, 999-1001, 1220, 1230-1242, 1255-1277, 1291-1336, 1498-1499
rest/types/__init__.py                           0       0  100.00%
rest/types/collections_search.py                13      13  0.00%    18-44
rest/types/eodag_search.py                     179       9  94.97%   52-55, 228-232, 285, 288, 356
rest/types/queryables.py                        58       2  96.55%   38, 172
rest/types/stac_search.py                      131      11  91.60%   51-54, 133, 179, 194-196, 204, 208
rest/utils/__init__.py                          95      13  86.32%   51, 108-109, 128-130, 182, 192-206
rest/utils/cql_evaluate.py                      48       5  89.58%   69, 76, 90, 97, 105
rest/utils/rfc3339.py                           23       3  86.96%   49, 61, 63
types/__init__.py                              110      11  90.00%   53, 70, 129-132, 199, 214-218, 245, 258
types/bbox.py                                   43      19  55.81%   46-61, 72-74, 85-87, 99-101, 113-115, 123
types/download_args.py                           9       0  100.00%
types/queryables.py                             81       0  100.00%
types/search_args.py                            70      18  74.29%   60-64, 71-88, 104
types/whoosh.py                                 15       0  100.00%
utils/__init__.py                              501      39  92.22%   84, 89, 110-112, 193-194, 203-230, 233, 247, 329-333, 409-413, 434-436, 518, 533, 571-572, 968-971, 979-980, 1021-1022, 1204
utils/constraints.py                           119      37  68.91%   94-103, 144, 149, 153, 164, 190-192, 202, 216-232, 241-252
utils/exceptions.py                             43       4  90.70%   23-25, 98-99
utils/import_system.py                          30      20  33.33%   27, 67-81, 93-103
utils/logging.py                                29       1  96.55%   123
utils/notebook.py                               44      23  47.73%   25-29, 36-41, 58-62, 72-78, 83-87
utils/requests.py                               55      11  80.00%   69, 94, 96, 98, 100, 102, 121, 126-128, 136
utils/rest.py                                   36       1  97.22%   57
utils/stac_reader.py                           111      45  59.46%   56-57, 63-85, 95-97, 101, 143, 159-162, 215-224, 234-264
TOTAL                                         9608    1719  82.11%
```

</details>


### Diff against develop

```
Filename                                 Stmts    Miss  Cover
-------------------------------------  -------  ------  -------
api/core.py                                 -4       0  -0.06%
api/search_result.py                        +8       0  +1.59%
plugins/search/data_request_search.py       +2      +2  -0.67%
plugins/search/qssearch.py                  -3       0  -0.05%
rest/core.py                                +2      +1  -0.31%
rest/errors.py                             +31     +18  +41.94%
rest/server.py                             -11     -13  +3.96%
rest/types/eodag_search.py                  -5       0  -0.14%
utils/exceptions.py                         +6      +2  -3.89%
TOTAL                                      +26     +10  -0.05%
```


Results for commit: 43ad8357c212dc86e7e4e7e5bf82188a82e39cd9


_Minimum allowed coverage is `70%`_


:recycle: This comment has been updated with latest results


<!-- Sticky Pull Request Commentartifacts/unit-test-results-python3.12-ubuntu-latest/coverage.xml -->
github-actions[bot]: ![badge](https://raw.githubusercontent.com/CS-SI/eodag/_xml_coverage_reports_win/data/wip-server-errors/./badge.svg)


## Code Coverage (Windows)

<details>


```
Filename                                     Stmts    Miss  Cover    Missing
-----------------------------------------  -------  ------  -------  ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
__init__.py                                     10       0  100.00%
cli.py                                         305      51  83.28%   60, 654-700, 802-853, 857
config.py                                      351      28  92.02%   81-83, 92, 100, 104-106, 183, 195, 460-462, 526-529, 576-577, 586-587, 666, 700, 735-740, 742
crunch.py                                        5       5  0.00%    20-24
api/__init__.py                                  0       0  100.00%
api/core.py                                    753      83  88.98%   90-99, 371, 596, 640-643, 681, 718, 791, 795-800, 826, 922, 1005, 1142, 1234-1246, 1286, 1288, 1316, 1320-1331, 1344-1350, 1440-1443, 1472-1492, 1548, 1565-1568, 1580-1583, 1601-1608, 1932, 1958-1964, 2231, 2235-2238, 2252-2254, 2289
api/search_result.py                            59       6  89.83%   35-37, 85, 94, 101, 115
api/product/__init__.py                          5       0  100.00%
api/product/_assets.py                          44       6  86.36%   27-29, 79, 119, 129
api/product/_product.py                        188      27  85.64%   56-63, 67-69, 239-240, 322, 351, 412, 426-429, 442, 466-469, 512-518
api/product/metadata_mapping.py                678      83  87.76%   67-69, 131-133, 234, 266-267, 313-314, 324-336, 338, 349, 355-367, 412-413, 450, 471-474, 497, 505-506, 582-583, 607-608, 614-617, 632-633, 782, 828, 1002-1007, 1138, 1152-1172, 1192, 1197, 1307, 1329, 1343, 1356-1375, 1414, 1466, 1504-1508, 1527
api/product/drivers/__init__.py                  5       0  100.00%
api/product/drivers/base.py                      8       2  75.00%   23, 41
plugins/__init__.py                              0       0  100.00%
plugins/base.py                                 23       4  82.61%   25, 48, 55, 68
plugins/manager.py                             133      15  88.72%   49-51, 105-110, 156, 197, 219, 223, 248, 287-288
plugins/apis/__init__.py                         0       0  100.00%
plugins/apis/base.py                             4       0  100.00%
plugins/apis/ecmwf.py                           99      15  84.85%   46-54, 150-152, 199-200, 226-228
plugins/apis/usgs.py                           175      36  79.43%   60-65, 132, 205, 239, 274-276, 281, 307-308, 313, 343-350, 361-366, 388-394, 396-402, 425
plugins/authentication/__init__.py               6       1  83.33%   31
plugins/authentication/aws_auth.py              20       2  90.00%   25-27
plugins/authentication/base.py                  19       3  84.21%   26, 34, 47
plugins/authentication/generic.py               16       3  81.25%   28, 40, 50
plugins/authentication/header.py                21       1  95.24%   28
plugins/authentication/keycloak.py              49       6  87.76%   33-35, 132, 156-161
plugins/authentication/oauth.py                 15       8  46.67%   25, 32-34, 38-41
plugins/authentication/openid_connect.py       185      19  89.73%   36-38, 114, 128-153, 161, 315-318, 342
plugins/authentication/qsauth.py                36       2  94.44%   32, 83
plugins/authentication/sas_auth.py              49       2  95.92%   32, 76
plugins/authentication/token.py                 90      17  81.11%   35-37, 79, 108, 110, 132-144, 199-202
plugins/authentication/token_exchange.py        35      19  45.71%   74-80, 92-120
plugins/crunch/__init__.py                       0       0  100.00%
plugins/crunch/base.py                          10       2  80.00%   25, 38
plugins/crunch/filter_date.py                   62      15  75.81%   30, 53-58, 72, 81, 90, 93, 105-107, 116-118, 125
plugins/crunch/filter_latest_intersect.py       50      35  30.00%   32-34, 48-53, 69-114
plugins/crunch/filter_latest_tpl_name.py        33       2  93.94%   28, 86
plugins/crunch/filter_overlap.py                68      19  72.06%   28-30, 33, 72-75, 82-85, 91, 99, 110-126
plugins/crunch/filter_property.py               33       8  75.76%   29, 60-65, 68-69, 85-89
plugins/download/__init__.py                     0       0  100.00%
plugins/download/aws.py                        492     165  66.46%   77-83, 272, 285, 352-355, 369-373, 419-421, 425, 459-460, 466-470, 503, 538, 542, 549, 579-587, 591, 629-637, 644-646, 687-761, 779-840, 851-856, 872-885, 914, 929-931, 934, 944-952, 960-973, 983-1002, 1009-1021, 1062, 1088, 1133-1135, 1355
plugins/download/base.py                       260      59  77.31%   58-64, 145, 180, 250-252, 319-320, 340-346, 377-381, 387-388, 432, 435-449, 461, 465, 538-542, 572-573, 581-598, 605-613, 615-619, 666, 688, 710, 718
plugins/download/creodias_s3.py                 17       9  47.06%   44-58
plugins/download/http.py                       531     132  75.14%   85-91, 197-209, 211-212, 247-250, 314-317, 319-320, 327-332, 350-365, 382-384, 396, 444, 451-457, 475, 489, 503, 511-513, 529-534, 545, 563, 605-609, 631, 671, 716, 730-736, 772-836, 854, 887-896, 922-923, 950-955, 961, 964, 980, 997-998, 1025-1026, 1033, 1095-1101, 1156-1157, 1163, 1173, 1207, 1243, 1261-1277
plugins/download/s3rest.py                     118      27  77.12%   55-58, 121, 162, 197, 227-234, 237-239, 243, 256-262, 270-271, 274-278, 301, 322-325
plugins/search/__init__.py                      26       3  88.46%   27-31
plugins/search/base.py                         133      18  86.47%   48-53, 104, 108, 121, 271, 291, 350-351, 371, 374-382, 384
plugins/search/build_search_result.py          183      31  83.06%   62, 97, 141-142, 148, 159, 295-298, 327, 384-401, 463, 466, 476, 493, 513-528
plugins/search/cop_marine.py                   200      49  75.50%   43-44, 55, 63-65, 71-72, 88, 90, 93, 128-130, 142-143, 183-192, 196, 199, 203, 221, 251, 255, 270, 274, 278, 282, 286-290, 296-299, 302-316, 333, 356, 359, 365
plugins/search/creodias_s3.py                   55       3  94.55%   61, 79, 113
plugins/search/csw.py                          108      83  23.15%   44-46, 58-59, 63-64, 72-120, 126-139, 147-179, 197-238
plugins/search/data_request_search.py          200      67  66.50%   53, 90-93, 109, 121, 125-126, 137, 142, 147, 154, 167-170, 224-225, 229, 239-245, 250, 276-279, 287-298, 315, 317, 324-327, 329-330, 348-352, 385, 392, 403, 416, 422-434, 439
plugins/search/qssearch.py                     645      97  84.96%   102, 386, 390-396, 404-405, 511-517, 567, 570, 583, 593, 612-627, 665-668, 742-743, 789, 808, 823, 881, 902, 905-906, 915, 926, 935, 958, 1029-1034, 1038-1039, 1072, 1146, 1195, 1269-1273, 1339-1340, 1361, 1388-1400, 1407, 1439-1441, 1451-1457, 1501, 1516, 1534, 1592-1663
plugins/search/static_stac_search.py            73      12  83.56%   37-38, 95-122, 138, 151
rest/__init__.py                                 4       2  50.00%   21-22
rest/cache.py                                   33      22  33.33%   35-37, 44-70
rest/config.py                                  26       4  84.62%   34-36, 68
rest/constants.py                                7       0  100.00%
rest/core.py                                   240     154  35.83%   84-88, 168-252, 276-330, 343-379, 416-448, 468-484, 507-518, 527-563, 582, 628-667, 706, 713-761
rest/errors.py                                  31      18  41.94%   51, 56-80, 85-88, 95
rest/server.py                                 275     275  0.00%    18-868
rest/stac.py                                   466     394  15.45%   65-67, 128-134, 142-165, 196-201, 233, 253-376, 386-455, 473-512, 528-581, 591-614, 624-664, 692-708, 718, 734-737, 756-826, 844-880, 909-934, 942-958, 966-971, 981-1008, 1018-1020, 1028-1030, 1043-1045, 1059-1076, 1086-1107, 1117-1139, 1147-1164, 1187-1210, 1220, 1230-1242, 1255-1277, 1291-1336, 1344-1361, 1371-1529
rest/types/__init__.py                           0       0  100.00%
rest/types/collections_search.py                13      13  0.00%    18-44
rest/types/eodag_search.py                     179      18  89.94%   52-55, 228-232, 265-267, 285, 288, 294, 298, 356, 368-371
rest/types/queryables.py                        58      14  75.86%   38, 51-52, 58-59, 65-66, 95-100, 109-110, 172
rest/types/stac_search.py                      131      22  83.21%   51-54, 131-133, 157-158, 163-164, 179, 194-196, 204, 208, 255-260
rest/utils/__init__.py                          95      31  67.37%   51, 79-85, 105, 108-109, 128-130, 143, 150, 175-183, 190-211
rest/utils/cql_evaluate.py                      48       5  89.58%   69, 76, 90, 97, 105
rest/utils/rfc3339.py                           23       6  73.91%   41, 44-45, 49, 61, 63
types/__init__.py                              110      36  67.27%   53, 66-70, 81-93, 120-122, 129-132, 172, 199, 209-221, 226, 245, 250, 258, 268
types/bbox.py                                   43      19  55.81%   46-61, 72-74, 85-87, 99-101, 113-115, 123
types/download_args.py                           9       0  100.00%
types/queryables.py                             81       0  100.00%
types/search_args.py                            70      18  74.29%   60-64, 71-88, 104
types/whoosh.py                                 15       0  100.00%
utils/__init__.py                              501      41  91.82%   84, 89, 110-112, 193-194, 203-230, 233, 247, 329-333, 409-413, 434-436, 518, 533, 571-572, 968-971, 979-980, 1021-1022, 1055, 1204, 1380
utils/constraints.py                           119      37  68.91%   94-103, 144, 149, 153, 164, 190-192, 202, 216-232, 241-252
utils/exceptions.py                             43       4  90.70%   23-25, 98-99
utils/import_system.py                          30      20  33.33%   27, 67-81, 93-103
utils/logging.py                                29       1  96.55%   123
utils/notebook.py                               44      23  47.73%   25-29, 36-41, 58-62, 72-78, 83-87
utils/requests.py                               55      11  80.00%   69, 94, 96, 98, 100, 102, 121, 126-128, 136
utils/rest.py                                   36       1  97.22%   57
utils/stac_reader.py                           111      45  59.46%   56-57, 63-85, 95-97, 101, 143, 159-162, 215-224, 234-264
TOTAL                                         9608    2514  73.83%
```

</details>


### Diff against develop

```
Filename                                 Stmts    Miss  Cover
-------------------------------------  -------  ------  --------
api/core.py                                 -4       0  -0.06%
api/search_result.py                        +8       0  +1.59%
plugins/search/data_request_search.py       +2      +2  -0.67%
plugins/search/qssearch.py                  -3       0  -0.07%
rest/core.py                                +2      +1  +0.12%
rest/errors.py                             +31     +18  +41.94%
rest/server.py                             -11     -11  +100.00%
rest/types/eodag_search.py                  -5      -1  +0.27%
utils/exceptions.py                         +6      +2  -3.89%
TOTAL                                      +26     +11  -0.05%
```


Results for commit: 43ad8357c212dc86e7e4e7e5bf82188a82e39cd9


_Minimum allowed coverage is `70%`_


:recycle: This comment has been updated with latest results


<!-- Sticky Pull Request Commentartifacts/unit-test-results-python3.12-windows-latest/coverage.xml -->
dalpasso: Please rebase

Patch:
diff --git a/eodag/api/core.py b/eodag/api/core.py
index 91bd0196..fb1325eb 100644
--- a/eodag/api/core.py
+++ b/eodag/api/core.py
@@ -217,7 +217,6 @@ class EODataAccessGateway:
                         os.path.join(self.conf_dir, "shp"),
                     )
         self.set_locations_conf(locations_conf_path)
-        self.search_errors: Set = set()
 
     def get_version(self) -> str:
         """Get eodag package version"""
@@ -1191,7 +1190,7 @@ class EODataAccessGateway:
             items_per_page=items_per_page,
         )
 
-        self.search_errors = set()
+        errors: List[Tuple[str, Exception]] = []
         # Loop over available providers and return the first non-empty results
         for i, search_plugin in enumerate(search_plugins):
             search_plugin.clear()
@@ -1201,17 +1200,19 @@ class EODataAccessGateway:
                 raise_errors=raise_errors,
                 **search_kwargs,
             )
+            errors.extend(search_results.errors)
             if len(search_results) == 0 and i < len(search_plugins) - 1:
                 logger.warning(
                     f"No result could be obtained from provider {search_plugin.provider}, "
                     "we will try to get the data from another provider",
                 )
             elif len(search_results) > 0:
+                search_results.errors = errors
                 return search_results
 
         if i > 1:
             logger.error("No result could be obtained from any available provider")
-        return SearchResult([], 0) if count else SearchResult([])
+        return SearchResult([], 0, errors) if count else SearchResult([], errors=errors)
 
     def search_iter_page(
         self,
@@ -1471,7 +1472,7 @@ class EODataAccessGateway:
                 )
                 or DEFAULT_MAX_ITEMS_PER_PAGE
             )
-            logger.debug(
+            logger.info(
                 "Searching for all the products with provider %s and a maximum of %s "
                 "items per page.",
                 search_plugin.provider,
@@ -1769,12 +1770,10 @@ class EODataAccessGateway:
             provider = preferred_provider
         providers = [plugin.provider for plugin in search_plugins]
         if provider not in providers:
-            logger.warning(
-                "Product type '%s' is not available with provider '%s'. "
-                "Searching it on provider '%s' instead.",
+            logger.debug(
+                "Product type '%s' is not available with preferred provider '%s'.",
                 product_type,
                 provider,
-                search_plugins[0].provider,
             )
         else:
             provider_plugin = list(
@@ -1782,11 +1781,6 @@ class EODataAccessGateway:
             )[0]
             search_plugins.remove(provider_plugin)
             search_plugins.insert(0, provider_plugin)
-            logger.info(
-                "Searching product type '%s' on provider: %s",
-                product_type,
-                search_plugins[0].provider,
-            )
         # Add product_types_config to plugin config. This dict contains product
         # type metadata that will also be stored in each product's properties.
         for search_plugin in search_plugins:
@@ -1830,6 +1824,7 @@ class EODataAccessGateway:
         :param kwargs: Some other criteria that will be used to do the search
         :returns: A collection of EO products matching the criteria
         """
+        logger.info("Searching on provider %s", search_plugin.provider)
         max_items_per_page = getattr(search_plugin.config, "pagination", {}).get(
             "max_items_per_page", DEFAULT_MAX_ITEMS_PER_PAGE
         )
@@ -1854,6 +1849,8 @@ class EODataAccessGateway:
         results: List[EOProduct] = []
         total_results: Optional[int] = 0 if count else None
 
+        errors: List[Tuple[str, Exception]] = []
+
         try:
             prep = PreparedSearch(count=count)
             if need_auth and auth_plugin and can_authenticate:
@@ -1948,13 +1945,6 @@ class EODataAccessGateway:
                         "the total number of products matching the search criteria"
                     )
         except Exception as e:
-            log_msg = f"No result from provider '{search_plugin.provider}' due to an error during search."
-            if not raise_errors:
-                log_msg += " Raise verbosity of log messages for details"
-            logger.info(log_msg)
-            # keep only the message from exception args
-            if len(e.args) > 1:
-                e.args = (e.args[0],)
             if raise_errors:
                 # Raise the error, letting the application wrapping eodag know that
                 # something went bad. This way it will be able to decide what to do next
@@ -1964,8 +1954,8 @@ class EODataAccessGateway:
                     "Error while searching on provider %s (ignored):",
                     search_plugin.provider,
                 )
-                self.search_errors.add((search_plugin.provider, e))
-        return SearchResult(results, total_results)
+                errors.append((search_plugin.provider, e))
+        return SearchResult(results, total_results, errors)
 
     def crunch(self, results: SearchResult, **kwargs: Any) -> SearchResult:
         """Apply the filters given through the keyword arguments to the results
diff --git a/eodag/api/search_result.py b/eodag/api/search_result.py
index 40931f2c..08417d69 100644
--- a/eodag/api/search_result.py
+++ b/eodag/api/search_result.py
@@ -18,9 +18,10 @@
 from __future__ import annotations
 
 from collections import UserList
-from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
+from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Tuple, Union
 
 from shapely.geometry import GeometryCollection, shape
+from typing_extensions import Doc
 
 from eodag.api.product import EOProduct
 from eodag.plugins.crunch.filter_date import FilterDate
@@ -28,6 +29,7 @@ from eodag.plugins.crunch.filter_latest_intersect import FilterLatestIntersect
 from eodag.plugins.crunch.filter_latest_tpl_name import FilterLatestByName
 from eodag.plugins.crunch.filter_overlap import FilterOverlap
 from eodag.plugins.crunch.filter_property import FilterProperty
+from eodag.utils import Annotated
 
 if TYPE_CHECKING:
     from shapely.geometry.base import BaseGeometry
@@ -47,11 +49,19 @@ class SearchResult(UserList):
 
     data: List[EOProduct]
 
+    errors: Annotated[
+        List[Tuple[str, Exception]], Doc("Tuple of provider name, exception")
+    ]
+
     def __init__(
-        self, products: List[EOProduct], number_matched: Optional[int] = None
+        self,
+        products: List[EOProduct],
+        number_matched: Optional[int] = None,
+        errors: List[Tuple[str, Exception]] = [],
     ) -> None:
-        super(SearchResult, self).__init__(products)
+        super().__init__(products)
         self.number_matched = number_matched
+        self.errors = errors
 
     def crunch(self, cruncher: Crunch, **search_params: Any) -> SearchResult:
         """Do some crunching with the underlying EO products.
@@ -197,6 +207,13 @@ class SearchResult(UserList):
             + "</table>"
         )
 
+    def extend(self, other: Iterable) -> None:
+        """override extend method to include errors"""
+        if isinstance(other, SearchResult):
+            self.errors.extend(other.errors)
+
+        return super().extend(other)
+
 
 class RawSearchResult(UserList):
     """An object representing a collection of raw/unparsed search results obtained from a provider.
diff --git a/eodag/plugins/apis/usgs.py b/eodag/plugins/apis/usgs.py
index 06023bd3..4e787410 100644
--- a/eodag/plugins/apis/usgs.py
+++ b/eodag/plugins/apis/usgs.py
@@ -256,7 +256,7 @@ class UsgsApi(Api):
                 f"Product type {usgs_dataset} may not exist on USGS EE catalog"
             )
             api.logout()
-            raise RequestError(e)
+            raise RequestError.from_error(e) from e
 
         api.logout()
 
diff --git a/eodag/plugins/authentication/generic.py b/eodag/plugins/authentication/generic.py
index 5d0271e2..9ec48228 100644
--- a/eodag/plugins/authentication/generic.py
+++ b/eodag/plugins/authentication/generic.py
@@ -48,6 +48,6 @@ class GenericAuth(Authentication):
             )
         else:
             raise MisconfiguredError(
-                f"Cannot authenticate with {self.provider}:",
-                f"method {method} is not supported. Must be one of digest or basic",
+                f"Cannot authenticate with {self.provider}",
+                f"Method {method} is not supported; it must be one of 'digest' or 'basic'.",
             )
diff --git a/eodag/plugins/authentication/openid_connect.py b/eodag/plugins/authentication/openid_connect.py
index 19493852..2c0cc1da 100644
--- a/eodag/plugins/authentication/openid_connect.py
+++ b/eodag/plugins/authentication/openid_connect.py
@@ -30,12 +30,7 @@ from requests.auth import AuthBase
 
 from eodag.plugins.authentication import Authentication
 from eodag.utils import HTTP_REQ_TIMEOUT, USER_AGENT, parse_qs, repeatfunc, urlparse
-from eodag.utils.exceptions import (
-    AuthenticationError,
-    MisconfiguredError,
-    RequestError,
-    TimeOutError,
-)
+from eodag.utils.exceptions import AuthenticationError, MisconfiguredError, TimeOutError
 
 if TYPE_CHECKING:
     from requests import PreparedRequest, Response
@@ -145,8 +140,9 @@ class OIDCRefreshTokenBase(Authentication):
             and e.response.status_code in auth_errors
         ):
             raise AuthenticationError(
-                "HTTP Error %s returned, %s\nPlease check your credentials for %s"
-                % (e.response.status_code, response_text, self.provider)
+                f"Please check your credentials for {self.provider}.",
+                f"HTTP Error {e.response.status_code} returned.",
+                response_text,
             )
         # other error
         else:
@@ -396,7 +392,7 @@ class OIDCAuthorizationCodeFlowAuth(OIDCRefreshTokenBase):
                 self.config.login_form_xpath.rstrip("/") + "/@action"
             )
             if not auth_uri or not auth_uri[0]:
-                raise RequestError(
+                raise MisconfiguredError(
                     f"Could not get auth_uri from {self.config.login_form_xpath}"
                 )
             auth_uri = auth_uri[0]
diff --git a/eodag/plugins/authentication/qsauth.py b/eodag/plugins/authentication/qsauth.py
index 26c92367..f85f6938 100644
--- a/eodag/plugins/authentication/qsauth.py
+++ b/eodag/plugins/authentication/qsauth.py
@@ -82,7 +82,7 @@ class HttpQueryStringAuth(Authentication):
             except requests.exceptions.Timeout as exc:
                 raise TimeOutError(exc, timeout=HTTP_REQ_TIMEOUT) from exc
             except RequestException as e:
-                raise AuthenticationError(f"Could no authenticate: {str(e)}")
+                raise AuthenticationError("Could no authenticate", str(e)) from e
 
         return auth
 
diff --git a/eodag/plugins/authentication/sas_auth.py b/eodag/plugins/authentication/sas_auth.py
index a40488d6..3965a263 100644
--- a/eodag/plugins/authentication/sas_auth.py
+++ b/eodag/plugins/authentication/sas_auth.py
@@ -75,7 +75,7 @@ class RequestsSASAuth(AuthBase):
             except requests.exceptions.Timeout as exc:
                 raise TimeOutError(exc, timeout=HTTP_REQ_TIMEOUT) from exc
             except (requests.RequestException, JSONDecodeError, KeyError) as e:
-                raise AuthenticationError(f"Could no get signed url: {str(e)}")
+                raise AuthenticationError("Could no get signed url", str(e)) from e
             else:
                 self.signed_urls[req_signed_url] = signed_url
 
diff --git a/eodag/plugins/authentication/token.py b/eodag/plugins/authentication/token.py
index 16006009..6cd89214 100644
--- a/eodag/plugins/authentication/token.py
+++ b/eodag/plugins/authentication/token.py
@@ -89,14 +89,15 @@ class TokenAuth(Authentication):
                 and e.response.status_code in auth_errors
             ):
                 raise AuthenticationError(
-                    f"HTTP Error {e.response.status_code} returned, {response_text}\n"
-                    f"Please check your credentials for {self.provider}"
-                )
+                    f"Please check your credentials for {self.provider}.",
+                    f"HTTP Error {e.response.status_code} returned.",
+                    response_text,
+                ) from e
             # other error
             else:
                 raise AuthenticationError(
-                    f"Could no get authentication token: {str(e)}, {response_text}"
-                )
+                    "Could no get authentication token", str(e), response_text
+                ) from e
         else:
             if getattr(self.config, "token_type", "text") == "json":
                 token = response.json()[self.config.token_key]
diff --git a/eodag/plugins/download/aws.py b/eodag/plugins/download/aws.py
index 21a45afc..d51d3cc5 100644
--- a/eodag/plugins/download/aws.py
+++ b/eodag/plugins/download/aws.py
@@ -620,15 +620,12 @@ class AwsDownload(Download):
 
     def _raise_if_auth_error(self, exception: ClientError) -> None:
         """Raises an error if given exception is an authentication error"""
-        err = exception.response["Error"]
+        err = cast(Dict[str, str], exception.response["Error"])
         if err["Code"] in AWS_AUTH_ERROR_MESSAGES and "key" in err["Message"].lower():
             raise AuthenticationError(
-                "HTTP error {} returned\n{}: {}\nPlease check your credentials for {}".format(
-                    exception.response["ResponseMetadata"]["HTTPStatusCode"],
-                    err["Code"],
-                    err["Message"],
-                    self.provider,
-                )
+                f"Please check your credentials for {self.provider}.",
+                f"HTTP Error {exception.response['ResponseMetadata']['HTTPStatusCode']} returned.",
+                err["Code"] + ": " + err["Message"],
             )
 
     def _stream_download_dict(
diff --git a/eodag/plugins/download/http.py b/eodag/plugins/download/http.py
index 01c82fd2..5c232556 100644
--- a/eodag/plugins/download/http.py
+++ b/eodag/plugins/download/http.py
@@ -837,12 +837,9 @@ class HTTPDownload(Download):
             and e.response.status_code in auth_errors
         ):
             raise AuthenticationError(
-                "HTTP Error %s returned, %s\nPlease check your credentials for %s"
-                % (
-                    e.response.status_code,
-                    response_text,
-                    self.provider,
-                )
+                f"Please check your credentials for {self.provider}.",
+                f"HTTP Error {e.response.status_code} returned.",
+                response_text,
             )
 
     def _process_exception(
@@ -1248,12 +1245,9 @@ class HTTPDownload(Download):
             auth_errors = [auth_errors]
         if e.response is not None and e.response.status_code in auth_errors:
             raise AuthenticationError(
-                "HTTP Error %s returned, %s\nPlease check your credentials for %s"
-                % (
-                    e.response.status_code,
-                    e.response.text.strip(),
-                    self.provider,
-                )
+                f"Please check your credentials for {self.provider}.",
+                f"HTTP Error {e.response.status_code} returned.",
+                e.response.text.strip(),
             )
         elif raise_errors:
             raise DownloadError(e)
diff --git a/eodag/plugins/download/s3rest.py b/eodag/plugins/download/s3rest.py
index b3553c2c..79dc5517 100644
--- a/eodag/plugins/download/s3rest.py
+++ b/eodag/plugins/download/s3rest.py
@@ -189,12 +189,9 @@ class S3RestDownload(Download):
                     auth_errors = [auth_errors]
                 if err.response and err.response.status_code in auth_errors:
                     raise AuthenticationError(
-                        "HTTP Error %s returned, %s\nPlease check your credentials for %s"
-                        % (
-                            err.response.status_code,
-                            err.response.text.strip(),
-                            self.provider,
-                        )
+                        f"Please check your credentials for {self.provider}.",
+                        f"HTTP Error {err.response.status_code} returned.",
+                        err.response.text.strip(),
                     )
                 # product not available
                 elif (
@@ -225,7 +222,7 @@ class S3RestDownload(Download):
                         self.__class__.__name__,
                         bucket_contents.text,
                     )
-                    raise RequestError(str(err))
+                    raise RequestError.from_error(err) from err
             try:
                 xmldoc = minidom.parseString(bucket_contents.text)
             except ExpatError as err:
diff --git a/eodag/plugins/search/build_search_result.py b/eodag/plugins/search/build_search_result.py
index 5bf5eb7c..ab400773 100644
--- a/eodag/plugins/search/build_search_result.py
+++ b/eodag/plugins/search/build_search_result.py
@@ -111,8 +111,8 @@ class BuildPostSearchResult(PostJsonSearch):
         prep.url = prep.search_urls[0]
         prep.info_message = f"Sending search request: {prep.url}"
         prep.exception_message = (
-            f"Skipping error while searching for {self.provider} "
-            f"{self.__class__.__name__} instance:"
+            f"Skipping error while searching for {self.provider}"
+            f" {self.__class__.__name__} instance"
         )
         response = self._request(prep)
 
diff --git a/eodag/plugins/search/creodias_s3.py b/eodag/plugins/search/creodias_s3.py
index dc5f0e75..62a224a6 100644
--- a/eodag/plugins/search/creodias_s3.py
+++ b/eodag/plugins/search/creodias_s3.py
@@ -29,7 +29,12 @@ from eodag.config import PluginConfig
 from eodag.plugins.authentication.aws_auth import AwsAuth
 from eodag.plugins.search.qssearch import ODataV4Search
 from eodag.utils import guess_file_type
-from eodag.utils.exceptions import AuthenticationError, MisconfiguredError, RequestError
+from eodag.utils.exceptions import (
+    AuthenticationError,
+    MisconfiguredError,
+    NotAvailableError,
+    RequestError,
+)
 
 DATA_EXTENSIONS = ["jp2", "tiff", "nc", "grib"]
 logger = logging.getLogger("eodag.search.creodiass3")
@@ -50,7 +55,7 @@ def patched_register_downloader(self, downloader, authenticator):
     try:
         _update_assets(self, downloader.config, authenticator)
     except BotoCoreError as e:
-        raise RequestError(f"could not update assets: {str(e)}") from e
+        raise RequestError.from_error(e, "could not update assets") from e
 
 
 def _update_assets(product: EOProduct, config: PluginConfig, auth: AwsAuth):
@@ -105,7 +110,9 @@ def _update_assets(product: EOProduct, config: PluginConfig, auth: AwsAuth):
                 raise AuthenticationError(
                     f"Authentication failed on {config.base_uri} s3"
                 ) from e
-            raise RequestError(f"assets for product {prefix} could not be found") from e
+            raise NotAvailableError(
+                f"assets for product {prefix} could not be found"
+            ) from e
 
 
 class CreodiasS3Search(ODataV4Search):
diff --git a/eodag/plugins/search/data_request_search.py b/eodag/plugins/search/data_request_search.py
index 7597ed6e..862abd4e 100644
--- a/eodag/plugins/search/data_request_search.py
+++ b/eodag/plugins/search/data_request_search.py
@@ -275,9 +275,9 @@ class DataRequestSearch(Search):
         except requests.exceptions.Timeout as exc:
             raise TimeOutError(exc, timeout=HTTP_REQ_TIMEOUT) from exc
         except requests.RequestException as e:
-            raise RequestError(
-                f"search job for product_type {product_type} could not be created: {str(e)}, {request_job.text}"
-            )
+            raise RequestError.from_error(
+                e, f"search job for product_type {product_type} could not be created"
+            ) from e
         else:
             logger.info("search job for product_type %s created", product_type)
             return request_job.json()["jobId"]
@@ -294,7 +294,7 @@ class DataRequestSearch(Search):
         except requests.exceptions.Timeout as exc:
             raise TimeOutError(exc, timeout=HTTP_REQ_TIMEOUT) from exc
         except requests.RequestException as e:
-            raise RequestError(f"_cancel_request failed: {str(e)}")
+            raise RequestError.from_error(e, "_cancel_request failed") from e
 
     def _check_request_status(self, data_request_id: str) -> bool:
         logger.debug("checking status of request job %s", data_request_id)
@@ -313,7 +313,7 @@ class DataRequestSearch(Search):
         except requests.exceptions.Timeout as exc:
             raise TimeOutError(exc, timeout=HTTP_REQ_TIMEOUT) from exc
         except requests.RequestException as e:
-            raise RequestError(f"_check_request_status failed: {str(e)}")
+            raise RequestError.from_error(e, "_check_request_status failed") from e
         else:
             status_data = status_resp.json()
             if "status_code" in status_data and status_data["status_code"] in [
@@ -321,7 +321,9 @@ class DataRequestSearch(Search):
                 404,
             ]:
                 logger.error(f"_check_request_status failed: {status_data}")
-                raise RequestError("authentication token expired during request")
+                error = RequestError("authentication token expired during request")
+                error.status_code = status_data["status_code"]
+                raise error
             if status_data["status"] == "failed":
                 logger.error(f"_check_request_status failed: {status_data}")
                 raise RequestError(
diff --git a/eodag/plugins/search/qssearch.py b/eodag/plugins/search/qssearch.py
index eff23ada..e235b121 100644
--- a/eodag/plugins/search/qssearch.py
+++ b/eodag/plugins/search/qssearch.py
@@ -857,8 +857,8 @@ class QueryStringSearch(Search):
                 search_url
             )
             single_search_prep.exception_message = (
-                "Skipping error while searching for {} {} "
-                "instance:".format(self.provider, self.__class__.__name__)
+                f"Skipping error while searching for {self.provider}"
+                f" {self.__class__.__name__} instance"
             )
             response = self._request(single_search_prep)
             next_page_url_key_path = self.config.pagination.get(
@@ -1177,7 +1177,7 @@ class QueryStringSearch(Search):
                     self.__class__.__name__,
                     err_msg,
                 )
-            raise RequestError(str(err))
+            raise RequestError.from_error(err, exception_message) from err
         return response
 
 
@@ -1222,7 +1222,7 @@ class ODataV4Search(QueryStringSearch):
                     raise TimeOutError(exc, timeout=HTTP_REQ_TIMEOUT) from exc
                 except requests.RequestException:
                     logger.exception(
-                        "Skipping error while searching for %s %s instance:",
+                        "Skipping error while searching for %s %s instance",
                         self.provider,
                         self.__class__.__name__,
                     )
@@ -1632,22 +1632,16 @@ class PostJsonSearch(QueryStringSearch):
         except requests.exceptions.Timeout as exc:
             raise TimeOutError(exc, timeout=timeout) from exc
         except (requests.RequestException, URLError) as err:
+            response = locals().get("response", Response())
             # check if error is identified as auth_error in provider conf
             auth_errors = getattr(self.config, "auth_error_code", [None])
             if not isinstance(auth_errors, list):
                 auth_errors = [auth_errors]
-            if (
-                hasattr(err, "response")
-                and err.response is not None
-                and getattr(err.response, "status_code", None)
-                and err.response.status_code in auth_errors
-            ):
+            if response.status_code and response.status_code in auth_errors:
                 raise AuthenticationError(
-                    "HTTP Error {} returned:\n{}\nPlease check your credentials for {}".format(
-                        err.response.status_code,
-                        err.response.text.strip(),
-                        self.provider,
-                    )
+                    f"Please check your credentials for {self.provider}.",
+                    f"HTTP Error {response.status_code} returned.",
+                    response.text.strip(),
                 )
             if exception_message:
                 logger.exception(exception_message)
@@ -1658,16 +1652,8 @@ class PostJsonSearch(QueryStringSearch):
                     self.provider,
                     self.__class__.__name__,
                 )
-            if "response" in locals():
-                logger.debug(response.content)
-            error_text = str(err)
-            if (
-                hasattr(err, "response")
-                and err.response is not None
-                and getattr(err.response, "text", None)
-            ):
-                error_text = err.response.text
-            raise RequestError(error_text) from err
+            logger.debug(response.content or str(err))
+            raise RequestError.from_error(err, exception_message) from err
         return response
 
 
diff --git a/eodag/rest/core.py b/eodag/rest/core.py
index c509594c..e6d18b5f 100644
--- a/eodag/rest/core.py
+++ b/eodag/rest/core.py
@@ -51,6 +51,7 @@ from eodag.rest.constants import (
     CACHE_KEY_COLLECTIONS,
     CACHE_KEY_QUERYABLES,
 )
+from eodag.rest.errors import ResponseSearchError
 from eodag.rest.stac import StacCatalog, StacCollection, StacCommon, StacItem
 from eodag.rest.types.eodag_search import EODAGSearch
 from eodag.rest.types.queryables import (
@@ -190,16 +191,17 @@ def search_stac_items(
 
     # get products by ids
     if eodag_args.ids:
-        search_results = SearchResult([])
+        results = SearchResult([])
         for item_id in eodag_args.ids:
-            sr = eodag_api.search(
-                id=item_id,
-                productType=catalogs[0] if catalogs else eodag_args.productType,
-                provider=eodag_args.provider,
+            results.extend(
+                eodag_api.search(
+                    id=item_id,
+                    productType=catalogs[0] if catalogs else eodag_args.productType,
+                    provider=eodag_args.provider,
+                )
             )
-            search_results.extend(sr)
-        search_results.number_matched = len(search_results)
-        total = len(search_results)
+        results.number_matched = len(results)
+        total = len(results)
 
     elif time_interval_overlap(eodag_args, catalog):
         criteria = {
@@ -217,18 +219,20 @@ def search_stac_items(
             criteria[new_key] = criteria[key]
             criteria.pop(key)
 
-        search_results = eodag_api.search(count=True, **criteria)
-        total = search_results.number_matched or 0
-        if search_request.crunch:
-            search_results = crunch_products(
-                search_results, search_request.crunch, **criteria
-            )
+        results = eodag_api.search(count=True, **criteria)
+        total = results.number_matched or 0
     else:
         # return empty results
-        search_results = SearchResult([], 0)
+        results = SearchResult([], 0)
         total = 0
 
-    for record in search_results:
+    if len(results) == 0 and results.errors:
+        raise ResponseSearchError(results.errors)
+
+    if search_request.crunch:
+        results = crunch_products(results, search_request.crunch, **criteria)
+
+    for record in results:
         record.product_type = eodag_api.get_alias_from_product_type(record.product_type)
 
     items = StacItem(
@@ -238,7 +242,7 @@ def search_stac_items(
         eodag_api=eodag_api,
         root=request.state.url_root,
     ).get_stac_items(
-        search_results=search_results,
+        search_results=results,
         total=total,
         next_link=get_next_link(
             request, search_request, total, eodag_args.items_per_page
diff --git a/eodag/rest/errors.py b/eodag/rest/errors.py
new file mode 100644
index 00000000..6ff067f4
--- /dev/null
+++ b/eodag/rest/errors.py
@@ -0,0 +1,178 @@
+# -*- coding: utf-8 -*-
+# Copyright 2024, CS GROUP - France, https://www.csgroup.eu/
+#
+# This file is part of EODAG project
+#     https://www.github.com/CS-SI/EODAG
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import logging
+from typing import Dict, List, Tuple, Union
+
+from fastapi import FastAPI, Request
+from fastapi.responses import ORJSONResponse
+from starlette import status
+from starlette.exceptions import HTTPException as StarletteHTTPException
+
+from eodag.rest.types.eodag_search import EODAGSearch
+from eodag.utils.exceptions import (
+    AuthenticationError,
+    DownloadError,
+    EodagError,
+    MisconfiguredError,
+    NoMatchingProductType,
+    NotAvailableError,
+    RequestError,
+    TimeOutError,
+    UnsupportedProductType,
+    UnsupportedProvider,
+    ValidationError,
+)
+
+EODAG_DEFAULT_STATUS_CODES = {
+    AuthenticationError: status.HTTP_500_INTERNAL_SERVER_ERROR,
+    DownloadError: status.HTTP_500_INTERNAL_SERVER_ERROR,
+    MisconfiguredError: status.HTTP_500_INTERNAL_SERVER_ERROR,
+    NotAvailableError: status.HTTP_404_NOT_FOUND,
+    NoMatchingProductType: status.HTTP_404_NOT_FOUND,
+    TimeOutError: status.HTTP_504_GATEWAY_TIMEOUT,
+    UnsupportedProductType: status.HTTP_404_NOT_FOUND,
+    UnsupportedProvider: status.HTTP_404_NOT_FOUND,
+    ValidationError: status.HTTP_400_BAD_REQUEST,
+}
+
+logger = logging.getLogger("eodag.rest.server")
+
+
+class ResponseSearchError(Exception):
+    """Represent a EODAG search error response"""
+
+    def __init__(self, errors: List[Tuple[str, Exception]]) -> None:
+        self._errors = errors
+
+    @property
+    def errors(self) -> List[Dict[str, Union[str, int]]]:
+        """return errors as a list of dict"""
+        error_list: List[Dict[str, Union[str, int]]] = []
+        for name, exception in self._errors:
+
+            error_dict: Dict[str, Union[str, int]] = {
+                "provider": name,
+                "error": exception.__class__.__name__,
+            }
+
+            if exception.args:
+                error_dict["message"] = exception.args[0]
+
+            if len(exception.args) > 1:
+                error_dict["detail"] = " ".join([str(i) for i in exception.args[1:]])
+
+            error_dict["status_code"] = EODAG_DEFAULT_STATUS_CODES.get(
+                type(exception), getattr(exception, "status_code", 500)
+            )
+
+            if type(exception) in (MisconfiguredError, AuthenticationError):
+                logger.error("%s: %s", type(exception).__name__, str(exception))
+                error_dict[
+                    "message"
+                ] = "Internal server error: please contact the administrator"
+                error_dict.pop("detail", None)
+
+            if type(exception) is ValidationError:
+                for error_param in exception.parameters:
+                    stac_param = EODAGSearch.to_stac(error_param)
+                    exception.message = exception.message.replace(
+                        error_param, stac_param
+                    )
+                error_dict["message"] = exception.message
+
+            error_list.append(error_dict)
+
+        return error_list
+
+    @property
+    def status_code(self) -> int:
+        """get global errors status code"""
+        if len(self._errors) == 1 and type(self.errors[0]["status_code"]) is int:
+            return self.errors[0]["status_code"]
+
+        return 400
+
+
+async def response_search_error_handler(
+    request: Request, exc: Exception
+) -> ORJSONResponse:
+    """Handle ResponseSearchError exceptions"""
+    if not isinstance(exc, ResponseSearchError):
+        return starlette_exception_handler(request, exc)
+
+    return ORJSONResponse(
+        status_code=exc.status_code,
+        content={"errors": exc.errors},
+    )
+
+
+async def eodag_errors_handler(request: Request, exc: Exception) -> ORJSONResponse:
+    """Handler for EODAG errors"""
+    if not isinstance(exc, EodagError):
+        return starlette_exception_handler(request, exc)
+
+    code = EODAG_DEFAULT_STATUS_CODES.get(type(exc), getattr(exc, "status_code", 500))
+    detail = f"{type(exc).__name__}: {str(exc)}"
+
+    if type(exc) in (MisconfiguredError, AuthenticationError, TimeOutError):
+        logger.error("%s: %s", type(exc).__name__, str(exc))
+
+    if type(exc) in (MisconfiguredError, AuthenticationError):
+        detail = "Internal server error: please contact the administrator"
+
+    if type(exc) is ValidationError:
+        for error_param in exc.parameters:
+            stac_param = EODAGSearch.to_stac(error_param)
+            exc.message = exc.message.replace(error_param, stac_param)
+        detail = exc.message
+
+    return ORJSONResponse(
+        status_code=code,
+        content={"description": detail},
+    )
+
+
+def starlette_exception_handler(request: Request, error: Exception) -> ORJSONResponse:
+    """Default errors handle"""
+    description = (
+        getattr(error, "description", None)
+        or getattr(error, "detail", None)
+        or str(error)
+    )
+    return ORJSONResponse(
+        status_code=getattr(error, "status_code", 500),
+        content={"description": description},
+    )
+
+
+def add_exception_handlers(app: FastAPI) -> None:
+    """Add exception handlers to the FastAPI application.
+
+    Args:
+        app: the FastAPI application.
+
+    Returns:
+        None
+    """
+    app.add_exception_handler(StarletteHTTPException, starlette_exception_handler)
+
+    app.add_exception_handler(RequestError, eodag_errors_handler)
+    for exc in EODAG_DEFAULT_STATUS_CODES:
+        app.add_exception_handler(exc, eodag_errors_handler)
+
+    app.add_exception_handler(ResponseSearchError, response_search_error_handler)
diff --git a/eodag/rest/server.py b/eodag/rest/server.py
index 4bbc3143..64d34db2 100755
--- a/eodag/rest/server.py
+++ b/eodag/rest/server.py
@@ -20,7 +20,6 @@ from __future__ import annotations
 import logging
 import os
 import re
-import traceback
 from contextlib import asynccontextmanager
 from importlib.metadata import version
 from json import JSONDecodeError
@@ -43,7 +42,6 @@ from fastapi.responses import ORJSONResponse
 from pydantic import ValidationError as pydanticValidationError
 from pygeofilter.backends.cql2_json import to_cql2
 from pygeofilter.parsers.cql2_text import parse as parse_cql2_text
-from starlette.exceptions import HTTPException as StarletteHTTPException
 
 from eodag.config import load_stac_api_config
 from eodag.rest.cache import init_cache
@@ -60,23 +58,11 @@ from eodag.rest.core import (
     get_stac_extension_oseo,
     search_stac_items,
 )
-from eodag.rest.types.eodag_search import EODAGSearch
+from eodag.rest.errors import add_exception_handlers
 from eodag.rest.types.queryables import QueryablesGetParams
 from eodag.rest.types.stac_search import SearchPostRequest, sortby2list
 from eodag.rest.utils import format_pydantic_error, str2json, str2list
 from eodag.utils import parse_header, update_nested_dict
-from eodag.utils.exceptions import (
-    AuthenticationError,
-    DownloadError,
-    MisconfiguredError,
-    NoMatchingProductType,
-    NotAvailableError,
-    RequestError,
-    TimeOutError,
-    UnsupportedProductType,
-    UnsupportedProvider,
-    ValidationError,
-)
 
 if TYPE_CHECKING:
     from fastapi.types import DecoratedCallable
@@ -85,12 +71,6 @@ if TYPE_CHECKING:
 from starlette.responses import Response as StarletteResponse
 
 logger = logging.getLogger("eodag.rest.server")
-ERRORS_WITH_500_STATUS_CODE = {
-    "MisconfiguredError",
-    "AuthenticationError",
-    "DownloadError",
-    "RequestError",
-}
 
 
 class APIRouter(FastAPIRouter):
@@ -198,6 +178,8 @@ app.add_middleware(
     allow_headers=["*"],
 )
 
+add_exception_handlers(app)
+
 
 @app.middleware("http")
 async def forward_middleware(
@@ -222,137 +204,6 @@ async def forward_middleware(
     return response
 
 
-@app.exception_handler(StarletteHTTPException)
-async def default_exception_handler(
-    request: Request, error: HTTPException
-) -> ORJSONResponse:
-    """Default errors handle"""
-    description = (
-        getattr(error, "description", None)
-        or getattr(error, "detail", None)
-        or str(error)
-    )
-    return ORJSONResponse(
-        status_code=error.status_code,
-        content={"description": description},
-    )
-
-
-@app.exception_handler(ValidationError)
-async def handle_invalid_usage_with_validation_error(
-    request: Request, error: ValidationError
-) -> ORJSONResponse:
-    """Invalid usage [400] ValidationError handle"""
-    if error.parameters:
-        for error_param in error.parameters:
-            stac_param = EODAGSearch.to_stac(error_param)
-            error.message = error.message.replace(error_param, stac_param)
-    logger.debug(traceback.format_exc())
-    return await default_exception_handler(
-        request,
-        HTTPException(
-            status_code=400,
-            detail=f"{type(error).__name__}: {str(error.message)}",
-        ),
-    )
-
-
-@app.exception_handler(NoMatchingProductType)
-@app.exception_handler(UnsupportedProductType)
-@app.exception_handler(UnsupportedProvider)
-async def handle_invalid_usage(request: Request, error: Exception) -> ORJSONResponse:
-    """Invalid usage [400] errors handle"""
-    return await default_exception_handler(
-        request,
-        HTTPException(
-            status_code=400,
-            detail=f"{type(error).__name__}: {str(error)}",
-        ),
-    )
-
-
-@app.exception_handler(NotAvailableError)
-async def handle_resource_not_found(
-    request: Request, error: Exception
-) -> ORJSONResponse:
-    """Not found [404] errors handle"""
-    return await default_exception_handler(
-        request,
-        HTTPException(
-            status_code=404,
-            detail=f"{type(error).__name__}: {str(error)}",
-        ),
-    )
-
-
-@app.exception_handler(MisconfiguredError)
-@app.exception_handler(AuthenticationError)
-async def handle_auth_error(request: Request, error: Exception) -> ORJSONResponse:
-    """These errors should be sent as internal server error to the client"""
-    logger.error("%s: %s", type(error).__name__, str(error))
-    return await default_exception_handler(
-        request,
-        HTTPException(
-            status_code=500,
-            detail="Internal server error: please contact the administrator",
-        ),
-    )
-
-
-@app.exception_handler(DownloadError)
-async def handle_download_error(request: Request, error: Exception) -> ORJSONResponse:
-    """DownloadError should be sent as internal server error with details to the client"""
-    logger.error(f"{type(error).__name__}: {str(error)}")
-    return await default_exception_handler(
-        request,
-        HTTPException(
-            status_code=500,
-            detail=f"{type(error).__name__}: {str(error)}",
-        ),
-    )
-
-
-@app.exception_handler(RequestError)
-async def handle_request_error(request: Request, error: RequestError) -> ORJSONResponse:
-    """RequestError should be sent as internal server error with details to the client"""
-    if getattr(error, "history", None):
-        error_history_tmp = list(error.history)
-        for i, search_error in enumerate(error_history_tmp):
-            if search_error[1].__class__.__name__ in ERRORS_WITH_500_STATUS_CODE:
-                search_error[1].args = ("an internal error occured",)
-                error_history_tmp[i] = search_error
-                continue
-            if getattr(error, "parameters", None):
-                for error_param in error.parameters:
-                    stac_param = EODAGSearch.to_stac(error_param)
-                    search_error[1].args = (
-                        search_error[1].args[0].replace(error_param, stac_param),
-                    )
-                    error_history_tmp[i] = search_error
-        error.history = set(error_history_tmp)
-    logger.error(f"{type(error).__name__}: {str(error)}")
-    return await default_exception_handler(
-        request,
-        HTTPException(
-            status_code=500,
-            detail=f"{type(error).__name__}: {str(error)}",
-        ),
-    )
-
-
-@app.exception_handler(TimeOutError)
-async def handle_timeout(request: Request, error: Exception) -> ORJSONResponse:
-    """Timeout [504] errors handle"""
-    logger.error(f"{type(error).__name__}: {str(error)}")
-    return await default_exception_handler(
-        request,
-        HTTPException(
-            status_code=504,
-            detail=f"{type(error).__name__}: {str(error)}",
-        ),
-    )
-
-
 @router.api_route(methods=["GET", "HEAD"], path="/", tags=["Capabilities"])
 async def catalogs_root(request: Request) -> ORJSONResponse:
     """STAC catalogs root"""
diff --git a/eodag/rest/types/eodag_search.py b/eodag/rest/types/eodag_search.py
index 3d546961..a2bf6da4 100644
--- a/eodag/rest/types/eodag_search.py
+++ b/eodag/rest/types/eodag_search.py
@@ -115,13 +115,6 @@ class EODAGSearch(BaseModel):
 
     _to_eodag_map: Dict[str, str]
 
-    @model_validator(mode="after")
-    def set_raise_errors(self) -> Self:
-        """Set raise_errors to True if provider is set"""
-        if self.provider:
-            self.raise_errors = True
-        return self
-
     @model_validator(mode="after")
     def remove_timeFromAscendingNode(self) -> Self:  # pylint: disable=invalid-name
         """TimeFromAscendingNode are just used for translation and not for search"""
diff --git a/eodag/utils/exceptions.py b/eodag/utils/exceptions.py
index 6dcc1236..02aa647c 100644
--- a/eodag/utils/exceptions.py
+++ b/eodag/utils/exceptions.py
@@ -20,7 +20,9 @@ from __future__ import annotations
 from typing import TYPE_CHECKING
 
 if TYPE_CHECKING:
-    from typing import Optional, Set, Tuple
+    from typing import Optional, Set
+
+    from typing_extensions import Annotated, Doc
 
 
 class EodagError(Exception):
@@ -84,14 +86,23 @@ class RequestError(EodagError):
     """An error indicating that a request has failed. Usually eodag functions
     and methods should catch and skip this"""
 
-    history: Set[Tuple[str, Exception]] = set()
-    parameters: Set[str] = set()
+    status_code: Annotated[Optional[int], Doc("HTTP status code")] = None
+
+    @classmethod
+    def from_error(cls, error: Exception, msg: Optional[str] = None):
+        """Generate a RequestError from an Exception"""
+        status_code = getattr(error, "code", None)
+        text = getattr(error, "msg", None)
+
+        if response := getattr(error, "response", None):
+            status_code = response.status_code
+            text = response.text
+
+        text = text or str(error)
 
-    def __str__(self):
-        repr = super().__str__()
-        for err_tuple in self.history:
-            repr += f"- {str(err_tuple)}"
-        return repr
+        e = cls(msg, text) if msg else cls(text)
+        e.status_code = status_code
+        return e
 
 
 class NoMatchingProductType(EodagError):
diff --git a/eodag/utils/requests.py b/eodag/utils/requests.py
index b8edfc76..3c35c21f 100644
--- a/eodag/utils/requests.py
+++ b/eodag/utils/requests.py
@@ -63,9 +63,7 @@ def fetch_json(
     except requests.exceptions.Timeout as exc:
         raise TimeOutError(exc, timeout=HTTP_REQ_TIMEOUT) from exc
     except requests.exceptions.RequestException as exc:
-        raise RequestError(
-            f"Unable to fetch {file_url}: {str(exc)}",
-        ) from exc
+        raise RequestError.from_error(exc, f"Unable to fetch {file_url}") from exc
     else:
         return res.json()
 


Test patch:
diff --git a/tests/context.py b/tests/context.py
index abb7c6c1..0b64d07f 100644
--- a/tests/context.py
+++ b/tests/context.py
@@ -102,11 +102,12 @@ from eodag.utils.exceptions import (
     NotAvailableError,
     PluginImplementationError,
     RequestError,
+    STACOpenerError,
+    TimeOutError,
     UnsupportedDatasetAddressScheme,
     UnsupportedProductType,
     UnsupportedProvider,
     ValidationError,
-    STACOpenerError,
 )
 from eodag.utils.stac_reader import fetch_stac_items, _TextOpener
 from tests import TEST_RESOURCES_PATH
diff --git a/tests/units/test_auth_plugins.py b/tests/units/test_auth_plugins.py
index a14c78c4..b3eec25b 100644
--- a/tests/units/test_auth_plugins.py
+++ b/tests/units/test_auth_plugins.py
@@ -28,7 +28,6 @@ from requests.exceptions import RequestException
 from eodag.config import override_config_from_mapping
 from eodag.plugins.authentication.openid_connect import CodeAuthorizedAuth
 from eodag.utils import MockResponse
-from eodag.utils.exceptions import RequestError
 from tests.context import (
     HTTP_REQ_TIMEOUT,
     USER_AGENT,
@@ -286,7 +285,7 @@ class TestAuthPluginTokenAuth(BaseAuthPluginTest):
 
         with self.assertRaisesRegex(
             AuthenticationError,
-            "Could no get authentication token: 404 .* test error message",
+            "('Could no get authentication token', '404 .* 'test error message')",
         ):
             # mock token post request response with a different status code from the one in the provider auth config
             with responses.RequestsMock(
@@ -311,7 +310,7 @@ class TestAuthPluginTokenAuth(BaseAuthPluginTest):
 
         with self.assertRaisesRegex(
             AuthenticationError,
-            f"HTTP Error 401 returned, test error message\nPlease check your credentials for {provider}",
+            f"('Please check your credentials for {provider}.', 'HTTP Error 401 returned.', 'test error message')",
         ):
             # mock token post request response with the same status code as the one in the provider auth config
             with responses.RequestsMock(
@@ -1510,7 +1509,7 @@ class TestAuthPluginOIDCAuthorizationCodeFlowAuth(BaseAuthPluginTest):
         """
         state = "1234567890123456789012"
         self.assertRaises(
-            RequestError,
+            MisconfiguredError,
             auth_plugin.authenticate_user,
             state,
         )
diff --git a/tests/units/test_http_server.py b/tests/units/test_http_server.py
index a5fef56e..f11a57ab 100644
--- a/tests/units/test_http_server.py
+++ b/tests/units/test_http_server.py
@@ -39,7 +39,12 @@ from eodag.plugins.download.base import Download
 from eodag.rest.config import Settings
 from eodag.rest.types.queryables import StacQueryables
 from eodag.utils import USER_AGENT, MockResponse, StreamResponse
-from eodag.utils.exceptions import NotAvailableError, TimeOutError
+from eodag.utils.exceptions import (
+    NotAvailableError,
+    RequestError,
+    TimeOutError,
+    ValidationError,
+)
 from tests import mock, temporary_environment
 from tests.context import (
     DEFAULT_ITEMS_PER_PAGE,
@@ -325,8 +330,12 @@ class RequestTestCase(unittest.TestCase):
         method: str = "GET",
         post_data: Optional[Any] = None,
         search_call_count: Optional[int] = None,
+        search_result: SearchResult = None,
     ) -> httpx.Response:
-        mock_search.return_value = self.mock_search_result()
+        if search_result:
+            mock_search.return_value = search_result
+        else:
+            mock_search.return_value = self.mock_search_result()
         response = self.app.request(
             method,
             url,
@@ -367,6 +376,7 @@ class RequestTestCase(unittest.TestCase):
         post_data: Optional[Any] = None,
         search_call_count: Optional[int] = None,
         check_links: bool = True,
+        search_result: SearchResult = None,
     ) -> Any:
         response = self._request_valid_raw(
             url,
@@ -374,6 +384,7 @@ class RequestTestCase(unittest.TestCase):
             method=method,
             post_data=post_data,
             search_call_count=search_call_count,
+            search_result=search_result,
         )
 
         # Assert response format is GeoJSON
@@ -836,7 +847,7 @@ class RequestTestCase(unittest.TestCase):
                 end="2018-02-01T00:00:00Z",
                 provider="peps",
                 geom=box(0, 43, 1, 44, ccw=False),
-                raise_errors=True,
+                raise_errors=False,
                 count=True,
             ),
         )
@@ -1046,7 +1057,7 @@ class RequestTestCase(unittest.TestCase):
             month="10",
             year="2010",
             day="10",
-            raise_errors=True,
+            raise_errors=False,
             count=True,
             provider="cop_cds",
         )
@@ -1106,6 +1117,91 @@ class RequestTestCase(unittest.TestCase):
             )
         )
 
+    def test_search_results_with_errors(self):
+        """Search through eodag server must not display provider's error if it's not empty result"""
+        errors = [
+            ("usgs", Exception("foo error")),
+            ("aws_eos", Exception("boo error")),
+        ]
+        search_results = self.mock_search_result()
+        search_results.errors.extend(errors)
+
+        self._request_valid(
+            f"search?collections={self.tested_product_type}",
+            search_result=search_results,
+        )
+
+    @mock.patch(
+        "eodag.rest.core.eodag_api.search",
+        autospec=True,
+    )
+    def test_search_no_results_with_errors(self, mock_search):
+        """Search through eodag server must display provider's error if it's empty result"""
+        req_err = RequestError("Request error message with status code")
+        req_err.status_code = 418
+        errors = [
+            ("usgs", Exception("Generic exception", "Details of the error")),
+            ("theia", TimeOutError("Timeout message")),
+            ("peps", req_err),
+            ("creodias", AuthenticationError("Authentication message")),
+            (
+                "onda",
+                ValidationError(
+                    "Validation message: startTimeFromAscendingNode, modificationDate",
+                    {"startTimeFromAscendingNode", "modificationDate"},
+                ),
+            ),
+        ]
+        expected_response = {
+            "errors": [
+                {
+                    "provider": "usgs",
+                    "error": "Exception",
+                    "message": "Generic exception",
+                    "detail": "Details of the error",
+                    "status_code": 500,
+                },
+                {
+                    "provider": "theia",
+                    "error": "TimeOutError",
+                    "message": "Timeout message",
+                    "status_code": 504,
+                },
+                {
+                    "provider": "peps",
+                    "error": "RequestError",
+                    "message": "Request error message with status code",
+                    "status_code": 418,
+                },
+                {
+                    "provider": "creodias",
+                    "error": "AuthenticationError",
+                    "message": "Internal server error: please contact the administrator",
+                    "status_code": 500,
+                },
+                {
+                    "provider": "onda",
+                    "error": "ValidationError",
+                    "message": "Validation message: start_datetime, updated",
+                    "detail": "{'startTimeFromAscendingNode', 'modificationDate'}",
+                    "status_code": 400,
+                },
+            ]
+        }
+        mock_search.return_value = SearchResult([], 0, errors)
+
+        response = self.app.request(
+            "GET",
+            "search?collections=S2_MSI_L1C",
+            json=None,
+            follow_redirects=True,
+            headers={},
+        )
+        response_content = json.loads(response.content.decode("utf-8"))
+
+        self.assertEqual(400, response.status_code)
+        self.assertDictEqual(expected_response, response_content)
+
     def test_assets_alt_url_blacklist(self):
         """Search through eodag server must not have alternate link if in blacklist"""
         # no blacklist
@@ -1500,7 +1596,7 @@ class RequestTestCase(unittest.TestCase):
         self.assertIn("description", response_content)
         self.assertIn("UnsupportedProvider", response_content["description"])
 
-        self.assertEqual(400, response.status_code)
+        self.assertEqual(404, response.status_code)
 
     @mock.patch("eodag.plugins.manager.PluginManager.get_auth_plugin", autospec=True)
     def test_product_type_queryables(self, mock_requests_session_post):
@@ -1626,7 +1722,7 @@ class RequestTestCase(unittest.TestCase):
         self.assertIn("description", response_content)
         self.assertIn("UnsupportedProductType", response_content["description"])
 
-        self.assertEqual(400, response.status_code)
+        self.assertEqual(404, response.status_code)
 
     @mock.patch("eodag.plugins.search.qssearch.requests.get", autospec=True)
     def test_product_type_queryables_with_provider(self, mock_requests_get):
diff --git a/tests/units/test_search_plugins.py b/tests/units/test_search_plugins.py
index 53fccc6b..5028bec6 100644
--- a/tests/units/test_search_plugins.py
+++ b/tests/units/test_search_plugins.py
@@ -39,7 +39,6 @@ from requests import RequestException
 
 from eodag.api.product.metadata_mapping import get_queryable_from_provider
 from eodag.utils import deepcopy
-from eodag.utils.exceptions import TimeOutError
 from tests.context import (
     DEFAULT_MISSION_START_DATE,
     HTTP_REQ_TIMEOUT,
@@ -49,9 +48,11 @@ from tests.context import (
     AuthenticationError,
     EOProduct,
     MisconfiguredError,
+    NotAvailableError,
     PluginManager,
     PreparedSearch,
     RequestError,
+    TimeOutError,
     cached_parse,
     get_geometry_from_various,
     load_default_config,
@@ -1442,7 +1443,7 @@ class TestSearchPluginODataV4Search(BaseSearchPluginTest):
                 )
             search_provider = self.onda_auth_plugin.provider
             search_type = self.onda_search_plugin.__class__.__name__
-            error_message = f"Skipping error while searching for {search_provider} {search_type} instance:"
+            error_message = f"Skipping error while searching for {search_provider} {search_type} instance"
             error_message_indexes_list = [
                 i.start() for i in re.finditer(error_message, str(cm.output))
             ]
@@ -2102,7 +2103,7 @@ class TestSearchPluginCreodiasS3Search(BaseSearchPluginTest):
             creodias_search_result = json.load(f)
         mock_request.return_value = MockResponse(creodias_search_result, 200)
 
-        with self.assertRaises(RequestError):
+        with self.assertRaises(NotAvailableError):
             res = search_plugin.query(productType="S1_SAR_GRD")
             for product in res[0]:
                 download_plugin = self.plugins_manager.get_download_plugin(product)