Improving tests and edge cases for URI and HTTP (#8497)

- Closes #8352
- ~~Proposed fix for #8493~~
- The temporary fix is deemed not viable. I will try to figure out a workaround and leave fixing #8493 to the engine team.
This commit is contained in:
Radosław Waśko 2023-12-15 18:58:45 +01:00 committed by GitHub
parent a1508beb8c
commit 940b8f7d51
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 1628 additions and 386 deletions

View File

@ -2326,8 +2326,9 @@ lazy val `std-base` = project
Compile / packageBin / artifactPath :=
`base-polyglot-root` / "std-base.jar",
libraryDependencies ++= Seq(
"org.graalvm.polyglot" % "polyglot" % graalMavenPackagesVersion,
"org.netbeans.api" % "org-openide-util-lookup" % netbeansApiVersion % "provided"
"org.graalvm.polyglot" % "polyglot" % graalMavenPackagesVersion,
"org.apache.httpcomponents" % "httpclient" % httpComponentsVersion,
"org.netbeans.api" % "org-openide-util-lookup" % netbeansApiVersion % "provided"
),
Compile / packageBin := Def.task {
val result = (Compile / packageBin).value
@ -2744,7 +2745,8 @@ lazy val `simple-httpbin` = project
Compile / run / mainClass := Some("org.enso.shttp.SimpleHTTPBin"),
assembly / mainClass := (Compile / run / mainClass).value,
libraryDependencies ++= Seq(
"org.apache.commons" % "commons-text" % commonsTextVersion
"org.apache.commons" % "commons-text" % commonsTextVersion,
"org.apache.httpcomponents" % "httpclient" % httpComponentsVersion
),
(Compile / run / fork) := true,
(Compile / run / connectInput) := true

View File

@ -6,6 +6,26 @@ The license information can be found along with the copyright notices.
Copyright notices related to this dependency can be found in the directory `com.ibm.icu.icu4j-73.1`.
'commons-codec', licensed under the The Apache Software License, Version 2.0, is distributed with the Base.
The license information can be found along with the copyright notices.
Copyright notices related to this dependency can be found in the directory `commons-codec.commons-codec-1.9`.
'commons-logging', licensed under the The Apache Software License, Version 2.0, is distributed with the Base.
The license file can be found at `licenses/APACHE2.0`.
Copyright notices related to this dependency can be found in the directory `commons-logging.commons-logging-1.2`.
'httpclient', licensed under the Apache License, Version 2.0, is distributed with the Base.
The license file can be found at `licenses/APACHE2.0`.
Copyright notices related to this dependency can be found in the directory `org.apache.httpcomponents.httpclient-4.4.1`.
'httpcore', licensed under the Apache License, Version 2.0, is distributed with the Base.
The license information can be found along with the copyright notices.
Copyright notices related to this dependency can be found in the directory `org.apache.httpcomponents.httpcore-4.4.1`.
'polyglot', licensed under the Universal Permissive License, Version 1.0, is distributed with the Base.
The license file can be found at `licenses/Universal_Permissive_License__Version_1.0`.
Copyright notices related to this dependency can be found in the directory `org.graalvm.polyglot.polyglot-23.1.0`.

View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

View File

@ -0,0 +1,9 @@
Apache Commons Codec
Copyright 2002-2013 The Apache Software Foundation
This product includes software developed at
The Apache Software Foundation (http://www.apache.org/).
src/test/org/apache/commons/codec/language/DoubleMetaphoneTest.java
contains test data from http://aspell.net/test/orig/batch0.tab.
Copyright (C) 2002 Kevin Atkinson (kevina@gnu.org)

View File

@ -0,0 +1,6 @@
Apache Commons Logging
Copyright 2003-2014 The Apache Software Foundation
This product includes software developed at
The Apache Software Foundation (http://www.apache.org/).

View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

View File

@ -0,0 +1,8 @@
Apache HttpClient
Copyright 1999-2015 The Apache Software Foundation
This product includes software developed at
The Apache Software Foundation (http://www.apache.org/).

View File

@ -0,0 +1,266 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.
=========================================================================
This project contains annotations in the package org.apache.http.annotation
which are derived from JCIP-ANNOTATIONS
Copyright (c) 2005 Brian Goetz and Tim Peierls.
See http://www.jcip.net and the Creative Commons Attribution License
(http://creativecommons.org/licenses/by/2.5)
Full text: http://creativecommons.org/licenses/by/2.5/legalcode
License
THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED.
BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. THE LICENSOR GRANTS YOU THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND CONDITIONS.
1. Definitions
"Collective Work" means a work, such as a periodical issue, anthology or encyclopedia, in which the Work in its entirety in unmodified form, along with a number of other contributions, constituting separate and independent works in themselves, are assembled into a collective whole. A work that constitutes a Collective Work will not be considered a Derivative Work (as defined below) for the purposes of this License.
"Derivative Work" means a work based upon the Work or upon the Work and other pre-existing works, such as a translation, musical arrangement, dramatization, fictionalization, motion picture version, sound recording, art reproduction, abridgment, condensation, or any other form in which the Work may be recast, transformed, or adapted, except that a work that constitutes a Collective Work will not be considered a Derivative Work for the purpose of this License. For the avoidance of doubt, where the Work is a musical composition or sound recording, the synchronization of the Work in timed-relation with a moving image ("synching") will be considered a Derivative Work for the purpose of this License.
"Licensor" means the individual or entity that offers the Work under the terms of this License.
"Original Author" means the individual or entity who created the Work.
"Work" means the copyrightable work of authorship offered under the terms of this License.
"You" means an individual or entity exercising rights under this License who has not previously violated the terms of this License with respect to the Work, or who has received express permission from the Licensor to exercise rights under this License despite a previous violation.
2. Fair Use Rights. Nothing in this license is intended to reduce, limit, or restrict any rights arising from fair use, first sale or other limitations on the exclusive rights of the copyright owner under copyright law or other applicable laws.
3. License Grant. Subject to the terms and conditions of this License, Licensor hereby grants You a worldwide, royalty-free, non-exclusive, perpetual (for the duration of the applicable copyright) license to exercise the rights in the Work as stated below:
to reproduce the Work, to incorporate the Work into one or more Collective Works, and to reproduce the Work as incorporated in the Collective Works;
to create and reproduce Derivative Works;
to distribute copies or phonorecords of, display publicly, perform publicly, and perform publicly by means of a digital audio transmission the Work including as incorporated in Collective Works;
to distribute copies or phonorecords of, display publicly, perform publicly, and perform publicly by means of a digital audio transmission Derivative Works.
For the avoidance of doubt, where the work is a musical composition:
Performance Royalties Under Blanket Licenses. Licensor waives the exclusive right to collect, whether individually or via a performance rights society (e.g. ASCAP, BMI, SESAC), royalties for the public performance or public digital performance (e.g. webcast) of the Work.
Mechanical Rights and Statutory Royalties. Licensor waives the exclusive right to collect, whether individually or via a music rights agency or designated agent (e.g. Harry Fox Agency), royalties for any phonorecord You create from the Work ("cover version") and distribute, subject to the compulsory license created by 17 USC Section 115 of the US Copyright Act (or the equivalent in other jurisdictions).
Webcasting Rights and Statutory Royalties. For the avoidance of doubt, where the Work is a sound recording, Licensor waives the exclusive right to collect, whether individually or via a performance-rights society (e.g. SoundExchange), royalties for the public digital performance (e.g. webcast) of the Work, subject to the compulsory license created by 17 USC Section 114 of the US Copyright Act (or the equivalent in other jurisdictions).
The above rights may be exercised in all media and formats whether now known or hereafter devised. The above rights include the right to make such modifications as are technically necessary to exercise the rights in other media and formats. All rights not expressly granted by Licensor are hereby reserved.
4. Restrictions.The license granted in Section 3 above is expressly made subject to and limited by the following restrictions:
You may distribute, publicly display, publicly perform, or publicly digitally perform the Work only under the terms of this License, and You must include a copy of, or the Uniform Resource Identifier for, this License with every copy or phonorecord of the Work You distribute, publicly display, publicly perform, or publicly digitally perform. You may not offer or impose any terms on the Work that alter or restrict the terms of this License or the recipients' exercise of the rights granted hereunder. You may not sublicense the Work. You must keep intact all notices that refer to this License and to the disclaimer of warranties. You may not distribute, publicly display, publicly perform, or publicly digitally perform the Work with any technological measures that control access or use of the Work in a manner inconsistent with the terms of this License Agreement. The above applies to the Work as incorporated in a Collective Work, but this does not require the Collective Work apart from the Work itself to be made subject to the terms of this License. If You create a Collective Work, upon notice from any Licensor You must, to the extent practicable, remove from the Collective Work any credit as required by clause 4(b), as requested. If You create a Derivative Work, upon notice from any Licensor You must, to the extent practicable, remove from the Derivative Work any credit as required by clause 4(b), as requested.
If you distribute, publicly display, publicly perform, or publicly digitally perform the Work or any Derivative Works or Collective Works, You must keep intact all copyright notices for the Work and provide, reasonable to the medium or means You are utilizing: (i) the name of the Original Author (or pseudonym, if applicable) if supplied, and/or (ii) if the Original Author and/or Licensor designate another party or parties (e.g. a sponsor institute, publishing entity, journal) for attribution in Licensor's copyright notice, terms of service or by other reasonable means, the name of such party or parties; the title of the Work if supplied; to the extent reasonably practicable, the Uniform Resource Identifier, if any, that Licensor specifies to be associated with the Work, unless such URI does not refer to the copyright notice or licensing information for the Work; and in the case of a Derivative Work, a credit identifying the use of the Work in the Derivative Work (e.g., "French translation of the Work by Original Author," or "Screenplay based on original Work by Original Author"). Such credit may be implemented in any reasonable manner; provided, however, that in the case of a Derivative Work or Collective Work, at a minimum such credit will appear where any other comparable authorship credit appears and in a manner at least as prominent as such other comparable authorship credit.
5. Representations, Warranties and Disclaimer
UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
7. Termination
This License and the rights granted hereunder will terminate automatically upon any breach by You of the terms of this License. Individuals or entities who have received Derivative Works or Collective Works from You under this License, however, will not have their licenses terminated provided such individuals or entities remain in full compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will survive any termination of this License.
Subject to the above terms and conditions, the license granted here is perpetual (for the duration of the applicable copyright in the Work). Notwithstanding the above, Licensor reserves the right to release the Work under different license terms or to stop distributing the Work at any time; provided, however that any such election will not serve to withdraw this License (or any other license that has been, or is required to be, granted under the terms of this License), and this License will continue in full force and effect unless terminated as stated above.
8. Miscellaneous
Each time You distribute or publicly digitally perform the Work or a Collective Work, the Licensor offers to the recipient a license to the Work on the same terms and conditions as the license granted to You under this License.
Each time You distribute or publicly digitally perform a Derivative Work, Licensor offers to the recipient a license to the original Work on the same terms and conditions as the license granted to You under this License.
If any provision of this License is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this License, and without further action by the parties to this agreement, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable.
No term or provision of this License shall be deemed waived and no breach consented to unless such waiver or consent shall be in writing and signed by the party to be charged with such waiver or consent.
This License constitutes the entire agreement between the parties with respect to the Work licensed here. There are no understandings, agreements or representations with respect to the Work not specified here. Licensor shall not be bound by any additional provisions that may appear in any communication from You. This License may not be modified without the mutual written agreement of the Licensor and You.

View File

@ -0,0 +1,10 @@
Apache HttpCore
Copyright 2005-2015 The Apache Software Foundation
This product includes software developed at
The Apache Software Foundation (http://www.apache.org/).
This project contains annotations derived from JCIP-ANNOTATIONS
Copyright (c) 2005 Brian Goetz and Tim Peierls. See http://www.jcip.net

View File

@ -0,0 +1,27 @@
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/

View File

@ -10,6 +10,7 @@ import project.Errors.Problem_Behavior.Problem_Behavior
import project.Meta
import project.Network.HTTP.Header.Header
import project.Network.HTTP.HTTP
import project.Network.HTTP.HTTP_Error.HTTP_Error
import project.Network.HTTP.HTTP_Method.HTTP_Method
import project.Network.HTTP.Request.Request
import project.Network.HTTP.Request_Body.Request_Body
@ -176,10 +177,14 @@ list_directory directory name_filter=Nothing recursive=False =
import Standard.Base.Data
file = enso_project.data / "spreadsheet.xls"
Data.fetch URL . body . to_file file
fetch : (URI | URI_With_Query | Text) -> HTTP_Method -> Vector (Header | Pair Text Text) -> Boolean -> Any
fetch : (URI | URI_With_Query | Text) -> HTTP_Method -> Vector (Header | Pair Text Text) -> Boolean -> Any ! Request_Error | HTTP_Error
fetch (uri:(URI | URI_With_Query | Text)) (method:HTTP_Method=HTTP_Method.Get) (headers:(Vector (Header | Pair Text Text))=[]) (try_auto_parse_response:Boolean=True) =
response = HTTP.fetch uri method headers
if try_auto_parse_response.not then response.with_materialized_body else
## We cannot catch decoding errors here and fall-back to the raw response
body, because as soon as decoding is started, at least part of the
input stream may already be consumed, so we cannot easily reconstruct
the whole stream.
response.decode if_unsupported=response.with_materialized_body
## ALIAS http post, upload
@ -300,7 +305,7 @@ fetch (uri:(URI | URI_With_Query | Text)) (method:HTTP_Method=HTTP_Method.Get) (
test_file = enso_project.data / "sample.txt"
form_data = Map.from_vector [["key", "val"], ["a_file", test_file]]
response = Data.post url_post (Request_Body.Form_Data form_data url_encoded=True)
post : (URI | URI_With_Query | Text) -> Request_Body -> HTTP_Method -> Vector (Header | Pair Text Text) -> Boolean -> Any
post : (URI | URI_With_Query | Text) -> Request_Body -> HTTP_Method -> Vector (Header | Pair Text Text) -> Boolean -> Any ! Request_Error | HTTP_Error
post (uri:(URI | URI_With_Query | Text)) (body:Request_Body=Request_Body.Empty) (method:HTTP_Method=HTTP_Method.Post) (headers:(Vector (Header | Pair Text Text))=[]) (try_auto_parse_response:Boolean=True) =
response = HTTP.post uri body method headers
if try_auto_parse_response.not then response.with_materialized_body else

View File

@ -12,6 +12,7 @@ import project.Errors.Common.Forbidden_Operation
import project.Errors.Illegal_Argument.Illegal_Argument
import project.Meta
import project.Network.HTTP.Header.Header
import project.Network.HTTP.HTTP_Error.HTTP_Error
import project.Network.HTTP.HTTP_Method.HTTP_Method
import project.Network.HTTP.HTTP_Version.HTTP_Version
import project.Network.HTTP.Request.Request
@ -91,7 +92,7 @@ type HTTP
- req: The HTTP request to send using `self` HTTP client.
- error_on_failure_code: Whether or not to throw an error if the response
code is not a success code.
request : Request -> Boolean -> Response ! Request_Error
request : Request -> Boolean -> Response ! Request_Error | HTTP_Error
request self req error_on_failure_code=True =
# Prevent request if the method is a write-like method and output context is disabled.
check_output_context ~action =
@ -133,11 +134,13 @@ type HTTP
response = Response.Value (EnsoSecretHelper.makeRequest self.internal_http_client builder uri_args.first.internal_uri uri_args.second mapped_headers)
if error_on_failure_code.not || response.code.is_success then response else
Error.throw (Request_Error.Error "Status Code" ("Request failed with status code: " + response.code.to_text + ". " + response.body.decode_as_text))
body = response.body.decode_as_text.catch Any _-> ""
message = if body.is_empty then Nothing else body
Error.throw (HTTP_Error.Status_Error response.code message response.uri)
## PRIVATE
Static helper for get-like methods
fetch : (URI | URI_With_Query | Text) -> HTTP_Method -> Vector (Header | Pair Text Text) -> Any
fetch : (URI | URI_With_Query | Text) -> HTTP_Method -> Vector (Header | Pair Text Text) -> Response ! Request_Error | HTTP_Error
fetch (uri:(URI | URI_With_Query | Text)) (method:HTTP_Method=HTTP_Method.Get) (headers:(Vector (Header | Pair Text Text))=[]) =
check_method fetch_methods method <|
request = Request.new method uri (parse_headers headers) Request_Body.Empty
@ -145,7 +148,7 @@ type HTTP
## PRIVATE
Static helper for post-like methods
post : (URI | URI_With_Query | Text) -> Request_Body -> HTTP_Method -> Vector (Header | Pair Text Text) -> Any
post : (URI | URI_With_Query | Text) -> Request_Body -> HTTP_Method -> Vector (Header | Pair Text Text) -> Response ! Request_Error | HTTP_Error
post (uri:(URI | URI_With_Query | Text)) (body:Request_Body=Request_Body.Empty) (method:HTTP_Method=HTTP_Method.Post) (headers:(Vector (Header | Pair Text Text))=[]) =
check_method post_methods method <|
request = Request.new method uri (parse_headers headers) body

View File

@ -2,6 +2,8 @@ import project.Any.Any
import project.Data.Text.Text
import project.Network.URI.URI
import project.Network.URI_With_Query.URI_With_Query
import project.Network.HTTP.HTTP_Status_Code.HTTP_Status_Code
import project.Nothing.Nothing
import project.Panic.Panic
polyglot java import java.io.IOException
@ -15,10 +17,25 @@ type HTTP_Error
- message: The message for the error.
IO_Error (uri:URI|URI_With_Query) (message:Text)
## An error indicating that a non-200 status code was returned.
Arguments:
- status_code: The status code that was returned.
- message: The message for the error, if it was able to be read.
- uri: The uri that couldn't be read.
Status_Error (status_code:HTTP_Status_Code) (message:Text|Nothing) (uri:URI|URI_With_Query)
## PRIVATE
Convert the HTTP_Error to a human-readable format.
to_display_text : Text
to_display_text self = self.message + " (" + self.uri.to_text + ")."
to_display_text self = case self of
HTTP_Error.IO_Error uri message ->
"IO Error: " + message + " (" + uri.to_display_text + ")."
HTTP_Error.Status_Error status_code message uri ->
prefix = "HTTP responded with status " + status_code.to_text
uri_part = " (at URI: " + uri.to_display_text + ")"
suffix = if message.is_nothing then "." else ": " + message.to_text + "."
prefix + uri_part + suffix
## PRIVATE
Utility method for running an action with Java exceptions mapping.

View File

@ -50,6 +50,12 @@ type HTTP_Method
HTTP_Method.Connect -> "CONNECT"
HTTP_Method.Custom verb -> verb
## PRIVATE
to_display_text : Text
to_display_text self = case self of
HTTP_Method.Custom verb -> "Custom: "+verb.to_display_text
_ -> self.to_http_method_name
## PRIVATE
Converts from Text to an HTTP_Method.
HTTP_Method.from (that:Text) = case that.to_case Case.Upper of

View File

@ -1,5 +1,7 @@
import project.Data.Boolean.Boolean
import project.Data.Numbers.Integer
import project.Data.Text.Text
from project.Data.Text.Extensions import all
type HTTP_Status_Code
## 100 Continue.
@ -166,12 +168,19 @@ type HTTP_Status_Code
Arguments:
- code: The numeric representation of the code.
Value code
Value code:Integer
## Does the status code represent a successful response?
is_success : Boolean
is_success self = self.code >= 200 && self.code < 300
## PRIVATE
to_text : Text
to_text self =
text_repr = self.to_display_text
if text_repr.starts_with "HTTP Status Code" then text_repr else
self.code.to_text + " " + text_repr
## PRIVATE
Convert to a display representation of this HTTP_Status_Code.
to_display_text : Text

View File

@ -46,6 +46,9 @@ type Response
## GROUP Metadata
Get the response headers.
It returns a vector of Header objects and not a mapping, because the
response may contain multiple headers with the same name.
> Example
Getting the headers from a response.
Note: This example will make a network request.
@ -53,10 +56,22 @@ type Response
import Standard.Examples
example_headers = Examples.get_response.headers
headers : Vector
> Example
Creating a mapping from the headers, throwing an error if there are duplicates.
Note: This example will make a network request.
import Standard.Examples
example_headers = Map.from_vector error_on_duplicates=True (Examples.get_response.headers.map h-> [h.name, h.value])
headers : Vector Header
headers self =
header_keys = self.internal_http_response.headerNames
header_keys.flat_map k-> (self.internal_http_response.headers.allValues k).map v-> Header.new k v
# This is a mapping that maps a header name to a list of values (since headers may be duplicated).
multi_map = self.internal_http_response.headers.map
multi_map.to_vector.flat_map p->
key = p.first
values = p.second
values.map v-> Header.new key v
## Get the response content type.
content_type : Text | Nothing

View File

@ -1,5 +1,6 @@
import project.Data.Enso_Cloud.Enso_Secret.Enso_Secret
import project.Data.Json.JS_Object
import project.Data.Numbers.Integer
import project.Data.Pair.Pair
import project.Data.Text.Text
import project.Data.Vector.Vector
@ -9,8 +10,8 @@ import project.Network.URI_With_Query.URI_With_Query
import project.Nothing.Nothing
import project.Panic.Panic
polyglot java import java.lang.Exception
polyglot java import java.net.URI as Java_URI
polyglot java import java.net.URISyntaxException
type URI
## ALIAS get uri
@ -31,7 +32,7 @@ type URI
example_parse = URI.parse "http://example.com"
parse : Text -> URI ! Syntax_Error
parse uri:Text =
Panic.catch Exception (URI.Value (Java_URI.create uri)) caught_panic->
Panic.catch URISyntaxException (URI.Value (Java_URI.new uri)) caught_panic->
message = caught_panic.payload.getMessage
truncated = if message.is_nothing || message.length > 100 then "Invalid URI '" + uri.to_display_text + "'" else
"URI syntax error: " + message
@ -112,10 +113,10 @@ type URI
import Standard.Examples
example_port = Examples.uri.port
port : Text | Nothing
port : Integer | Nothing
port self =
port_number = self.internal_uri.getPort
if port_number == -1 then Nothing else port_number.to_text
if port_number == -1 then Nothing else port_number
## GROUP Metadata
Get the path part of this URI.

View File

@ -14,14 +14,15 @@ import project.Nothing.Nothing
import project.Panic.Panic
from project.Data.Boolean import Boolean, False, True
polyglot java import java.lang.Exception
polyglot java import java.net.URI as Java_URI
polyglot java import java.net.URISyntaxException
polyglot java import org.enso.base.enso_cloud.EnsoSecretHelper
polyglot java import org.enso.base.net.URIHelpers
## Represents a URI with a set of query parameters
type URI_With_Query
## PRIVATE
Value uri:URI parameters:Vector
Value uri:URI (parameters : Vector (Pair Text (Text | Enso_Secret)))
## PRIVATE
Convert this to URI.
@ -29,19 +30,13 @@ type URI_With_Query
are used.
to_uri : URI ! Enso_Secret_Error
to_uri self =
Panic.catch Exception (URI.Value (EnsoSecretHelper.replaceQuery self.uri.internal_uri self.query)) caught_panic->
message = caught_panic.payload.getMessage
Error.throw (Syntax_Error.Error "Unable to collapse to a URI:"+message)
java_params = make_java_parameters self.get_raw_parameters
URI.Value (build_java_uri_with_parameters self.uri.internal_uri java_params)
## GROUP Metadata
Get the query part of this URI, but will error if any secrets are used.
query : Text | Nothing ! Enso_Secret_Error
query self =
base_query = self.uri.query.if_nothing ""
query_params = self.parameters.map p->
if p.second.is_a Enso_Secret then Error.throw Enso_Secret_Error.Access_Denied else
(EnsoSecretHelper.encodeArg p.first True) + "=" + (EnsoSecretHelper.encodeArg p.second False)
(if base_query == "" then "" else base_query + "&") + (query_params.join "&")
query self = self.to_uri.query
## GROUP Calculations
Adds a query parameter to the URI
@ -53,19 +48,25 @@ type URI_With_Query
add_query_argument self key:Text value:(Text | Enso_Secret) =
URI_With_Query.Value self.uri self.parameters+[Pair.new key value]
## PRIVATE
Returns a list of parameters as name-value pairs.
It will fail if any secrets are used.
get_raw_parameters : Vector (Pair Text Text) ! Enso_Secret_Error
get_raw_parameters self =
self.parameters.map p-> case p.second of
_ : Enso_Secret -> Error.throw Enso_Secret_Error.Access_Denied
_ : Text -> p
## PRIVATE
Convert this URI to text.
to_text : Text
to_text self =
base_query = self.uri.query.if_nothing ""
query_params = self.parameters.map p->
if p.second.is_a Enso_Secret then p.first + "=__SECRET__" else
p.first + "=" + p.second
new_query = "?" + (if base_query == "" then "" else base_query + "&") + (query_params.join "&")
Panic.catch Exception (EnsoSecretHelper.replaceQuery self.uri.internal_uri new_query . toString) caught_panic->
message = caught_panic.payload.getMessage
Error.throw (Syntax_Error.Error "Unable to render URI_With_Query:"+message)
masked_parameters = self.parameters.map p-> case p.second of
_ : Enso_Secret -> Pair.new p.first "__SECRET__"
_ : Text -> p
java_params = make_java_parameters masked_parameters
java_uri = build_java_uri_with_parameters self.uri.internal_uri java_params
java_uri.to_text
## PRIVATE
Convert to a display representation of this URI.
@ -114,3 +115,16 @@ type URI_With_Query
Get the fragment part of this URI.
fragment : Text | Nothing
fragment self = self.uri.fragment
## PRIVATE
make_java_parameters params = params.map p->
URIHelpers.NameValuePair.new p.first p.second
## PRIVATE
build_java_uri_with_parameters java_uri java_params =
Panic.catch URISyntaxException (URIHelpers.addQueryParameters java_uri java_params) caught_panic->
message = caught_panic.payload.getMessage
Error.throw (Syntax_Error.Error "Unable to collapse to a URI: "+message)
## PRIVATE
URI.from (that : URI_With_Query) = that.to_uri

View File

@ -0,0 +1,8 @@
package org.enso.base.enso_cloud;
import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpHeaders;
/** A subset of the HttpResponse to avoid leaking the decrypted Enso secrets. */
public record EnsoHttpResponse(URI uri, HttpHeaders headers, InputStream body, int statusCode) {}

View File

@ -1,11 +1,11 @@
package org.enso.base.enso_cloud;
import org.enso.base.net.URIHelpers;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.http.HttpRequest.Builder;
import java.net.http.HttpResponse;
import java.sql.Connection;
@ -13,8 +13,6 @@ import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.List;
import java.util.Properties;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Makes HTTP requests with secrets in either header or query string.
@ -22,21 +20,21 @@ import java.util.stream.Collectors;
public class EnsoSecretHelper {
/**
* Gets the value of an EnsoKeyValuePair resolving secrets.
*
* @param pair The pair to resolve.
* @return The pair's value. Should not be returned to Enso.
*/
private static String resolveValue(EnsoKeyValuePair pair) {
return switch (pair) {
case EnsoKeyStringPair stringPair -> stringPair.value();
case EnsoKeySecretPair secretPair ->
EnsoSecretReader.readSecret(secretPair.secretId());
case null ->
throw new IllegalArgumentException("EnsoKeyValuePair should not be NULL.");
case EnsoKeySecretPair secretPair -> EnsoSecretReader.readSecret(secretPair.secretId());
case null -> throw new IllegalArgumentException("EnsoKeyValuePair should not be NULL.");
};
}
/**
* Converts an EnsoKeyValuePair into a string for display purposes. Does not include secrets.
*
* @param pair The pair to render.
* @return The rendered string.
*/
@ -44,45 +42,13 @@ public class EnsoSecretHelper {
return switch (pair) {
case EnsoKeyStringPair stringPair -> stringPair.value();
case EnsoKeySecretPair _ -> "__SECRET__";
case null ->
throw new IllegalArgumentException("EnsoKeyValuePair should not be NULL.");
case null -> throw new IllegalArgumentException("EnsoKeyValuePair should not be NULL.");
};
}
/**
* Substitutes the minimal parts within the string for the URI parse.
* */
public static String encodeArg(String arg, boolean includeEquals) {
var encoded = arg.replace("%", "%25")
.replace("&", "%26")
.replace(" ", "%20");
if (includeEquals) {
encoded = encoded.replace("=", "%3D");
}
return encoded;
}
/**
* Replaces the query string in a URI.
* */
public static URI replaceQuery(URI uri, String newQuery) throws URISyntaxException {
var baseURI = new URI(uri.getScheme(), uri.getAuthority(), uri.getPath(), null, null).toString();
var baseFragment = uri.getFragment();
baseFragment = baseFragment != null && !baseFragment.isBlank() ? "#" + baseFragment : "";
return URI.create(baseURI + newQuery + baseFragment);
}
private static String makeQueryAry(EnsoKeyValuePair pair, Function<EnsoKeyValuePair, String> resolver) {
String resolvedKey = pair.key() != null && !pair.key().isBlank() ? encodeArg(pair.key(), true) + "=" : "";
String resolvedValue = encodeArg(resolver.apply(pair), false);
return resolvedKey + resolvedValue;
}
//** Gets a JDBC connection resolving EnsoKeyValuePair into the properties. **//
public static Connection getJDBCConnection(String url, EnsoKeyValuePair[] properties)
throws SQLException {
throws SQLException {
var javaProperties = new Properties();
for (EnsoKeyValuePair pair : properties) {
javaProperties.setProperty(pair.key(), resolveValue(pair));
@ -92,31 +58,36 @@ public class EnsoSecretHelper {
}
//** Makes a request with secrets in the query string or headers. **//
public static EnsoHttpResponse makeRequest(HttpClient client, Builder builder, URI uri, List<EnsoKeyValuePair> queryArguments, List<EnsoKeyValuePair> headerArguments)
public static EnsoHttpResponse makeRequest(HttpClient client, Builder builder, URI uri,
List<EnsoKeyValuePair> queryArguments,
List<EnsoKeyValuePair> headerArguments)
throws IOException, InterruptedException {
// Build a new URI with the query arguments.
URI resolvedURI = uri;
URI renderedURI = uri;
if (queryArguments != null && !queryArguments.isEmpty()) {
boolean hasSecrets = queryArguments.stream().anyMatch(p -> p instanceof EnsoKeySecretPair);
if (hasSecrets && !uri.getScheme().equals("https")) {
// If used a secret then only allow HTTPS
throw new IllegalArgumentException("Cannot use secrets in query string with non-HTTPS URI, but the scheme " +
"was: " + uri.getScheme() + ".");
}
try {
var baseQuery = uri.getQuery();
baseQuery = baseQuery != null && !baseQuery.isBlank() ? "?" + baseQuery + "&" : "?";
var query = baseQuery + queryArguments.stream().map(p -> makeQueryAry(p, EnsoSecretHelper::resolveValue)).collect(Collectors.joining("&"));
List<URIHelpers.NameValuePair> resolvedArguments = queryArguments.stream()
.map(p -> new URIHelpers.NameValuePair(p.key(), resolveValue(p)))
.toList();
List<URIHelpers.NameValuePair> renderedArguments = queryArguments.stream()
.map(p -> new URIHelpers.NameValuePair(p.key(), renderValue(p)))
.toList();
resolvedURI = replaceQuery(uri, query);
renderedURI = resolvedURI;
if (queryArguments.stream().anyMatch(p -> p instanceof EnsoKeySecretPair)) {
if (!resolvedURI.getScheme().equals("https")) {
// If used a secret then only allow HTTPS
throw new IllegalArgumentException("Cannot use secrets in query string with non-HTTPS URI.");
}
var renderedQuery = baseQuery + queryArguments.stream().map(p -> makeQueryAry(p, EnsoSecretHelper::renderValue)).collect(Collectors.joining("&"));
renderedURI = replaceQuery(uri, renderedQuery);
}
resolvedURI = URIHelpers.addQueryParameters(uri, resolvedArguments);
renderedURI = URIHelpers.addQueryParameters(uri, renderedArguments);
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Unable to build a valid URI.");
throw new IllegalStateException(
"Unexpectedly unable to build a valid URI from the base URI: " + uri + " and query arguments: " + queryArguments + "."
);
}
}
builder.uri(resolvedURI);
@ -134,11 +105,6 @@ public class EnsoSecretHelper {
var javaResponse = client.send(httpRequest, bodyHandler);
// Extract parts of the response
return new EnsoHttpResponse(renderedURI, javaResponse.headers().map().keySet().stream().toList(), javaResponse.headers(), javaResponse.body(), javaResponse.statusCode());
return new EnsoHttpResponse(renderedURI, javaResponse.headers(), javaResponse.body(), javaResponse.statusCode());
}
/**
* A subset of the HttpResponse to avoid leaking the decrypted Enso secrets.
*/
public record EnsoHttpResponse(URI uri, List<String> headerNames, HttpHeaders headers, InputStream body, int statusCode) { }
}

View File

@ -0,0 +1,19 @@
package org.enso.base.net;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import org.apache.http.client.utils.URIBuilder;
public class URIHelpers {
public record NameValuePair(String name, String value) {}
public static URI addQueryParameters(URI uri, List<NameValuePair> params)
throws URISyntaxException {
URIBuilder builder = new URIBuilder(uri);
for (NameValuePair param : params) {
builder.addParameter(param.name(), param.value());
}
return builder.build();
}
}

View File

@ -29,9 +29,16 @@ spec =
r.to Table . should_equal expected_table
Test.specify "fetching csv" <|
r = Data.fetch base_url_with_slash+"testfiles/table.csv"
url = base_url_with_slash+"testfiles/table.csv"
r = Data.fetch url
expected_table = Table.from_rows ["A", "B"] [[1, "x"], [3, "y"]]
r.to Table . should_equal expected_table
r.should_be_a Table
r.should_equal expected_table
r2 = url.to_uri.fetch
r2.should_be_a Table
r2.should_equal expected_table
Test.specify "fetching xls" <|
url = base_url_with_slash+"testfiles/table.xls"
@ -44,7 +51,7 @@ spec =
r2 = Data.fetch url try_auto_parse_response=False . decode (Excel (Excel_Section.Worksheet "MyTestSheet"))
r2.should_be_a Table
r2 . should_equal expected_table
r2.should_equal expected_table
Test.specify "fetching xlsx" <|
url = base_url_with_slash+"testfiles/table.xlsx"
@ -57,4 +64,8 @@ spec =
r2 = Data.fetch url try_auto_parse_response=False . decode (Excel (Excel_Section.Worksheet "MyTestSheet"))
r2.should_be_a Table
r2 . should_equal expected_table
r2.should_equal expected_table
r3 = url.to_uri.fetch try_auto_parse_response=False . decode (Excel (Excel_Section.Worksheet "MyTestSheet"))
r3.should_be_a Table
r3.should_equal expected_table

View File

@ -1,5 +1,6 @@
from Standard.Base import all
import Standard.Base.Errors.Common.Index_Out_Of_Bounds
import Standard.Base.Errors.Common.No_Such_Method
import Standard.Base.Errors.Illegal_Argument.Illegal_Argument
import Standard.Base.Errors.No_Such_Key.No_Such_Key
from Standard.Base.Data.Json import Invalid_JSON
@ -68,6 +69,7 @@ spec =
Test.specify "should be able to deserialize using into via conversion" <|
Json.parse '{"type":"Time_Zone","constructor":"parse","id":"Europe/Moscow"}' . into Time_Zone . should_equal (Time_Zone.parse "Europe/Moscow")
Json.parse '{}' . into Time_Zone . should_fail_with Illegal_Argument
Test.specify "should be able to deserialize using into for single constructor" <|
Json.parse '{"first": 1, "second": 2}' . into Pair . should_equal (Pair.Value 1 2)
@ -78,6 +80,14 @@ spec =
Json.parse '{"constructor": "Less", "than": 2}' . into Filter_Condition . should_equal (Filter_Condition.Less 2)
Json.parse '{"constructor": "NotARealOne", "than": 2}' . into Filter_Condition . should_fail_with Illegal_Argument
Test.specify "should be able to convert a JS_Object into a Map using into" <|
Json.parse '{"a": 15, "b": 20, "c": "X", "d": null}' . into Map . should_equal (Map.from_vector [["a", 15], ["b", 20], ["c", "X"], ["d", Nothing]])
Json.parse '{}' . into Map . should_equal Map.empty
# [] parses as a vector/array which does not have the `into` method, that only works for {} objects:
Test.expect_panic No_Such_Method <|
Json.parse '[]' . into Map
Test.specify "should be able to deserialize Date" <|
'{"type": "Date", "constructor": "new", "year": 2018, "month": 7, "day": 3}'.should_parse_as (Date.new 2018 7 3)
'{"type": "Date", "year": 2025, "month": 5, "day": 12}'.should_parse_as (Date.new 2025 5 12)

View File

@ -3,10 +3,13 @@ from Standard.Base import all
import Standard.Base.Errors.Common.Forbidden_Operation
import Standard.Base.Errors.Common.Syntax_Error
import Standard.Base.Errors.Illegal_Argument.Illegal_Argument
import Standard.Base.Network.HTTP.HTTP_Error.HTTP_Error
import Standard.Base.Network.HTTP.Request.Request
import Standard.Base.Network.HTTP.Response.Response
import Standard.Base.Network.HTTP.Request_Body.Request_Body
import Standard.Base.Network.HTTP.Request_Error
import Standard.Base.Network.Proxy.Proxy
import Standard.Base.Network.URI_With_Query.URI_With_Query
import Standard.Base.Runtime.Context
from Standard.Base.Network.HTTP import resolve_headers
@ -14,8 +17,6 @@ import Standard.Test.Extensions
from Standard.Test import Test, Test_Suite
from Standard.Test.Execution_Context_Helpers import run_with_and_without_output
polyglot java import java.lang.System as Java_System
type Test_Type
Aaa (s:Text)
Bbb (i:Integer)
@ -28,9 +29,11 @@ spec =
## To run this test locally:
$ sbt 'simple-httpbin/run localhost 8080'
$ export ENSO_HTTP_TEST_HTTPBIN_URL=http://localhost:8080/
base_url = Java_System.getenv "ENSO_HTTP_TEST_HTTPBIN_URL"
base_url = Environment.get "ENSO_HTTP_TEST_HTTPBIN_URL"
pending_has_url = if base_url != Nothing then Nothing else
"The HTTP tests only run when the `ENSO_HTTP_TEST_HTTPBIN_URL` environment variable is set to URL of the httpbin server"
base_url_with_slash = base_url.if_not_nothing <|
if base_url.ends_with "/" then base_url else base_url + "/"
Test.group "HTTP_Method parse" <|
Test.specify "should be able to parse a string value into a method" <|
@ -48,6 +51,18 @@ spec =
Test.specify "should make a custom method" <|
"CUSTOM" . to HTTP_Method . should_equal (HTTP_Method.Custom "CUSTOM")
Test.group "HTTP_Status_Code" <|
Test.specify "should have a nice text representation" <|
s1 = HTTP_Status_Code.ok
s1.code . should_equal 200
s1.to_text . should_equal "200 OK"
s1.to_display_text . should_equal "OK"
s2 = HTTP_Status_Code.not_found
s2.code . should_equal 404
s2.to_text . should_equal "404 Not Found"
s2.to_display_text . should_equal "Not Found"
Test.group "HTTP client" pending=pending_has_url <|
Test.specify "should create HTTP client with timeout setting" <|
http = HTTP.new (timeout = (Duration.new seconds=30))
@ -68,7 +83,6 @@ spec =
http.version.should_equal version_setting
Test.group "fetch" pending=pending_has_url <|
base_url_with_slash = if base_url.ends_with "/" then base_url else base_url + "/"
url_get = base_url_with_slash + "get"
url_head = base_url_with_slash + "head"
url_options = base_url_with_slash + "options"
@ -85,13 +99,13 @@ spec =
"Content-Length": "0"
},
"origin": "127.0.0.1",
"url": "",
"path": "/get",
"method": "GET",
"args": {}
}
response . should_equal expected_response
uri_response = url_get.to_uri.fetch
uri_response = url_get.to URI . fetch
uri_response . should_equal expected_response
Test.specify "Can perform a HEAD" <|
@ -116,6 +130,9 @@ spec =
response = Data.fetch url_get
response.at "headers" . at "Content-Length" . should_equal "0"
uri_response = url_get.to_uri.fetch
uri_response.at "headers" . at "Content-Length" . should_equal "0"
Test.specify "Can skip auto-parse" <|
response = Data.fetch url_get try_auto_parse_response=False
response.code.code . should_equal 200
@ -129,7 +146,7 @@ spec =
"Content-Length": "0"
},
"origin": "127.0.0.1",
"url": "",
"path": "/get",
"method": "GET",
"args": {}
}
@ -147,11 +164,11 @@ spec =
Test.specify "Unsupported method" <|
err = Data.fetch url_get method=HTTP_Method.Post
err.catch.should_equal (Illegal_Argument.Error "Unsupported method Post")
err.catch.should_equal (Illegal_Argument.Error "Unsupported method POST")
Test.specify "Cannot DELETE through fetch" <|
err = Data.fetch url_get method=HTTP_Method.Delete
err.catch.should_equal (Illegal_Argument.Error "Unsupported method Delete")
err.catch.should_equal (Illegal_Argument.Error "Unsupported method DELETE")
Test.specify "unknown host" <|
Data.fetch "http://undefined_host.invalid" . should_fail_with Request_Error
@ -160,8 +177,19 @@ spec =
Data.fetch "zxcv://bad.scheme" . should_fail_with Request_Error
Data.fetch "" . should_fail_with Request_Error
Test.specify "can select the version" <|
req = Request.get url_get
r2 = HTTP.new version=HTTP_Version.HTTP_2 . request req . decode_as_json
r2.at "headers" . at "Connection" . should_equal "Upgrade, HTTP2-Settings"
r2.at "headers" . at "Http2-Settings" . should_contain "AA"
r1 = HTTP.new version=HTTP_Version.HTTP_1_1 . request req . decode_as_json
header_names = r1.at "headers" . field_names . map (s-> s.to_case Case.Lower)
header_names.should_not_contain "connection"
header_names.should_not_contain "http2-settings"
header_names.should_not_contain "upgrade"
Test.group "post" pending=pending_has_url <|
base_url_with_slash = if base_url.ends_with "/" then base_url else base_url + "/"
url_post = base_url_with_slash + "post"
url_put = base_url_with_slash + "put"
url_patch = base_url_with_slash + "patch"
@ -169,24 +197,7 @@ spec =
Test.specify "Can perform a Request_Body.Text POST" <|
response = Data.post url_post (Request_Body.Text "hello world")
expected_response = Json.parse <| '''
{
"headers": {
"Connection": "Upgrade, HTTP2-Settings",
"Http2-Settings": "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA",
"User-Agent": "Java-http-client/21.0.1",
"Upgrade": "h2c",
"Content-Type": "text/plain; charset=UTF-8",
"Content-Length": "11"
},
"origin": "127.0.0.1",
"url": "",
"method": "POST",
"form": null,
"files": null,
"data": "hello world",
"args": {}
}
expected_response = echo_response_template "POST" "/post" "hello world" content_type="text/plain; charset=UTF-8"
response . should_equal expected_response
url_response = url_post.to_uri.post (Request_Body.Text "hello world")
@ -195,139 +206,44 @@ spec =
Test.specify "Can perform a Request_Body.Json JSON POST" <|
json = Json.parse '{"a": "asdf", "b": 123}'
response = Data.post url_post (Request_Body.Json json)
expected_response = Json.parse <| '''
{
"headers": {
"Connection": "Upgrade, HTTP2-Settings",
"Http2-Settings": "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA",
"User-Agent": "Java-http-client/21.0.1",
"Upgrade": "h2c",
"Content-Type": "application/json",
"Content-Length": "20"
},
"origin": "127.0.0.1",
"url": "",
"method": "POST",
"form": null,
"files": null,
"data": "{\\"a\\":\\"asdf\\",\\"b\\":123}",
"args": {}
}
expected_response = echo_response_template "POST" "/post" '{"a":"asdf","b":123}' content_type="application/json"
response . should_equal expected_response
Test.specify "Can perform a JSON POST" <|
json = Json.parse '{"a": "asdf", "b": 123}'
response = Data.post url_post json
expected_response = Json.parse <| '''
{
"headers": {
"Connection": "Upgrade, HTTP2-Settings",
"Http2-Settings": "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA",
"User-Agent": "Java-http-client/21.0.1",
"Upgrade": "h2c",
"Content-Type": "application/json",
"Content-Length": "20"
},
"origin": "127.0.0.1",
"url": "",
"method": "POST",
"form": null,
"files": null,
"data": "{\\"a\\":\\"asdf\\",\\"b\\":123}",
"args": {}
}
expected_response = echo_response_template "POST" "/post" '{"a":"asdf","b":123}' content_type="application/json"
response . should_equal expected_response
Test.specify "Can perform an object Request_Body.Json POST" <|
response = Data.post url_post (Request_Body.Json (Test_Type.Aaa "abc"))
expected_response = Json.parse <| '''
{
"headers": {
"Connection": "Upgrade, HTTP2-Settings",
"Http2-Settings": "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA",
"User-Agent": "Java-http-client/21.0.1",
"Upgrade": "h2c",
"Content-Type": "application/json",
"Content-Length": "50"
},
"origin": "127.0.0.1",
"url": "",
"method": "POST",
"form": null,
"files": null,
"data": "{\\"type\\":\\"Test_Type\\",\\"constructor\\":\\"Aaa\\",\\"s\\":\\"abc\\"}",
"args": {}
}
expected_response = echo_response_template "POST" "/post" '{"type":"Test_Type","constructor":"Aaa","s":"abc"}' content_type="application/json"
response . should_equal expected_response
Test.specify "Can perform an object JSON POST" <|
response = Data.post url_post (Test_Type.Bbb 12)
expected_response = Json.parse <| '''
{
"headers": {
"Connection": "Upgrade, HTTP2-Settings",
"Http2-Settings": "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA",
"User-Agent": "Java-http-client/21.0.1",
"Upgrade": "h2c",
"Content-Type": "application/json",
"Content-Length": "47"
},
"origin": "127.0.0.1",
"url": "",
"method": "POST",
"form": null,
"files": null,
"data": "{\\"type\\":\\"Test_Type\\",\\"constructor\\":\\"Bbb\\",\\"i\\":12}",
"args": {}
}
expected_response = echo_response_template "POST" "/post" '{"type":"Test_Type","constructor":"Bbb","i":12}' content_type="application/json"
response . should_equal expected_response
uri_response = url_post.to_uri.post (Test_Type.Bbb 12)
uri_response . should_equal expected_response
Test.specify "can handle a bad .to_json" <|
Data.post url_post (Bad_To_Json.Aaa "abcd") . should_fail_with Illegal_Argument
Test.specify "Can perform a Text POST with explicit encoding" <|
body = Request_Body.Text 'Hello World!' encoding=Encoding.utf_16_le
response = Data.post url_post body
expected_response = Json.parse <| '''
{
"headers": {
"Connection": "Upgrade, HTTP2-Settings",
"Http2-Settings": "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA",
"User-Agent": "Java-http-client/21.0.1",
"Upgrade": "h2c",
"Content-Type": "text/plain; charset=UTF-16LE",
"Content-Length": "24"
},
"origin": "127.0.0.1",
"url": "",
"method": "POST",
"form": null,
"files": null,
"data": "Hello World!",
"args": {}
}
expected_response = echo_response_template "POST" "/post" "Hello World!" content_type="text/plain; charset=UTF-16LE" content_length=24
response . should_equal expected_response
uri_response = url_post.to_uri.post body
uri_response . should_equal expected_response
Test.specify "Can perform a Text POST with explicit content type" <|
response = Data.post url_post (Request_Body.Text 'a,b,c\n' content_type="text/csv")
expected_response = Json.parse <| '''
{
"headers": {
"Connection": "Upgrade, HTTP2-Settings",
"Http2-Settings": "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA",
"User-Agent": "Java-http-client/21.0.1",
"Upgrade": "h2c",
"Content-Type": "text/csv; charset=UTF-8",
"Content-Length": "6"
},
"origin": "127.0.0.1",
"url": "",
"method": "POST",
"form": null,
"files": null,
"data": "a,b,c\\n",
"args": {}
}
expected_response = echo_response_template "POST" "/post" 'a,b,c\n' content_type="text/csv; charset=UTF-8"
response . should_equal expected_response
Test.specify "Can perform a File POST" <|
@ -341,6 +257,7 @@ spec =
test_file = enso_project.data / "sample.png"
response = Data.post url_post (Request_Body.Binary test_file)
response.at "headers" . at "Content-Type" . should_equal "application/octet-stream"
response.at "headers" . at "Content-Length" . should_equal test_file.size.to_text
response.at "data" . should_start_with '\uFFFDPNG'
Test.specify "Can perform a url-encoded form POST" <|
@ -367,68 +284,17 @@ spec =
Test.specify "Can perform a Text POST with auto-conversion" <|
response = Data.post url_post "hello world"
expected_response = Json.parse <| '''
{
"headers": {
"Connection": "Upgrade, HTTP2-Settings",
"Http2-Settings": "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA",
"User-Agent": "Java-http-client/21.0.1",
"Upgrade": "h2c",
"Content-Type": "text/plain; charset=UTF-8",
"Content-Length": "11"
},
"origin": "127.0.0.1",
"url": "",
"method": "POST",
"form": null,
"files": null,
"data": "hello world",
"args": {}
}
expected_response = echo_response_template "POST" "/post" "hello world" content_type="text/plain; charset=UTF-8"
response . should_equal expected_response
Test.specify "Can perform a Request_Body.Text PUT" <|
response = Data.post url_put (Request_Body.Text "hello world") method=HTTP_Method.Put
expected_response = Json.parse <| '''
{
"headers": {
"Connection": "Upgrade, HTTP2-Settings",
"Http2-Settings": "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA",
"User-Agent": "Java-http-client/21.0.1",
"Upgrade": "h2c",
"Content-Type": "text/plain; charset=UTF-8",
"Content-Length": "11"
},
"origin": "127.0.0.1",
"url": "",
"method": "PUT",
"form": null,
"files": null,
"data": "hello world",
"args": {}
}
expected_response = echo_response_template "PUT" "/put" "hello world" content_type="text/plain; charset=UTF-8"
response . should_equal expected_response
Test.specify "Can perform a Request_Body.Text PATCH" <|
response = Data.post url_patch (Request_Body.Text "hello world" content_type="application/diff") method=HTTP_Method.Patch
expected_response = Json.parse <| '''
{
"headers": {
"Connection": "Upgrade, HTTP2-Settings",
"Http2-Settings": "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA",
"User-Agent": "Java-http-client/21.0.1",
"Upgrade": "h2c",
"Content-Type": "application/diff; charset=UTF-8",
"Content-Length": "11"
},
"origin": "127.0.0.1",
"url": "",
"method": "PATCH",
"form": null,
"files": null,
"data": "hello world",
"args": {}
}
expected_response = echo_response_template "PATCH" "/patch" "hello world" content_type="application/diff; charset=UTF-8"
response . should_equal expected_response
Test.specify "Can perform a DELETE" <|
@ -443,7 +309,7 @@ spec =
"Content-Length": "0"
},
"origin": "127.0.0.1",
"url": "",
"path": "/delete",
"method": "DELETE",
"form": null,
"files": null,
@ -454,28 +320,11 @@ spec =
Test.specify "Can skip auto-parse" <|
response = Data.post url_post (Request_Body.Text "hello world") try_auto_parse_response=False
expected_response = Json.parse <| '''
{
"headers": {
"Connection": "Upgrade, HTTP2-Settings",
"Http2-Settings": "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA",
"User-Agent": "Java-http-client/21.0.1",
"Upgrade": "h2c",
"Content-Type": "text/plain; charset=UTF-8",
"Content-Length": "11"
},
"origin": "127.0.0.1",
"url": "",
"method": "POST",
"form": null,
"files": null,
"data": "hello world",
"args": {}
}
expected_response = echo_response_template "POST" "/post" "hello world" content_type="text/plain; charset=UTF-8"
response.decode_as_json . should_equal expected_response
Test.specify "Can send a custom header" <|
response = Data.post url_post (Request_Body.Text "hello world") headers=[Header.new "Custom" "asdf"]
response = Data.post url_post (Request_Body.Text "hello world") headers=[Header.new "Custom" "asdf", Header.new "Another" 'a:b: c - "ddd"']
expected_response = Json.parse <| '''
{
"headers": {
@ -485,10 +334,11 @@ spec =
"Upgrade": "h2c",
"Content-Type": "text/plain; charset=UTF-8",
"Content-Length": "11",
"Custom": "asdf"
"Custom": "asdf",
"Another": "a:b: c - \\"ddd\\""
},
"origin": "127.0.0.1",
"url": "",
"path": "/post",
"method": "POST",
"form": null,
"files": null,
@ -497,6 +347,37 @@ spec =
}
response . should_equal expected_response
Test.specify "can handle HTTP errors" <|
# This should give us 405 method not allowed
r1 = Data.post url_delete
r1.should_fail_with HTTP_Error
r1.catch.should_be_a HTTP_Error.Status_Error
r1.catch.status_code.code . should_equal 405
r1.catch.to_display_text . should_contain "status 405"
uri2 = URI.from (base_url_with_slash + "some/unknown/path")
r2 = Data.post uri2
r2.should_fail_with HTTP_Error
r2.catch.should_be_a HTTP_Error.Status_Error
r2.catch.status_code.code . should_equal 404
r2.catch.message . should_contain "<h1>404 Not Found</h1>"
r2.catch.uri . should_equal uri2
r3 = HTTP.new.request (Request.new (HTTP_Method.Custom "BREW_COFFEE") (base_url_with_slash + "get"))
r3.should_fail_with HTTP_Error
r3.catch.should_be_a HTTP_Error.Status_Error
r3.catch.status_code.code . should_equal 400
# Also test the URI_With_Query variant
uri4 = uri2.add_query_argument "a" "b" . add_query_argument "c" "d"
uri4.should_be_a URI_With_Query
r4 = uri4.fetch
r4.should_fail_with HTTP_Error
r4.catch.should_be_a HTTP_Error.Status_Error
r4.catch.status_code.code . should_equal 404
# The error may not necessarily store the URI_With_Query but a raw URI:
r4.catch.uri . should_equal (URI.from uri4)
Test.specify "Cannot perform POST when output context is disabled" <|
Context.Output.with_disabled <|
Data.post url_post (Request_Body.Text "hello world") . should_fail_with Forbidden_Operation
@ -510,7 +391,10 @@ spec =
Test.specify "Unsupported method" <|
err = Data.post url_post (Request_Body.Text "hello world") method=HTTP_Method.Get
err.catch.should_equal (Illegal_Argument.Error "Unsupported method Get")
err.catch.should_equal (Illegal_Argument.Error "Unsupported method GET")
err2 = Data.post url_post (Request_Body.Text "hello world") method=(HTTP_Method.Custom "BREW_COFFEE")
err2.catch.should_equal (Illegal_Argument.Error "Unsupported method Custom: BREW_COFFEE")
Test.specify "unknown host" <|
Data.post "http://undefined_host.invalid" (Request_Body.Text "hello world") . should_fail_with Request_Error
@ -520,7 +404,6 @@ spec =
Data.post url_post (Request_Body.Binary test_file) . should_fail_with Request_Error
Test.group "Headers" pending=pending_has_url <|
base_url_with_slash = if base_url.ends_with "/" then base_url else base_url + "/"
url_post = base_url_with_slash + "post"
Test.specify "Content-type in the body is respected" <|
@ -536,7 +419,7 @@ spec =
"Content-Length": "23"
},
"origin": "127.0.0.1",
"url": "",
"path": "/post",
"method": "POST",
"form": null,
"files": null,
@ -558,7 +441,7 @@ spec =
"Content-Length": "23"
},
"origin": "127.0.0.1",
"url": "",
"path": "/post",
"method": "POST",
"form": null,
"files": null,
@ -569,6 +452,8 @@ spec =
Test.specify "Multiple content types in the header list are respected" <|
response = Data.post url_post (Request_Body.Text '{"a": "asdf", "b": 123}') headers=[Header.content_type "application/json", Header.content_type "text/plain"]
## Our simple-httpbin server gets 2 Content-Type headers and merges them in the response.
How this is interpreted in practice depends on the server.
expected_response = Json.parse <| '''
{
"headers": {
@ -576,11 +461,11 @@ spec =
"Http2-Settings": "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA",
"User-Agent": "Java-http-client/21.0.1",
"Upgrade": "h2c",
"Content-Type": "application/json",
"Content-Type": "application/json, text/plain",
"Content-Length": "23"
},
"origin": "127.0.0.1",
"url": "",
"path": "/post",
"method": "POST",
"form": null,
"files": null,
@ -602,7 +487,7 @@ spec =
"Content-Length": "23"
},
"origin": "127.0.0.1",
"url": "",
"path": "/post",
"method": "POST",
"form": null,
"files": null,
@ -617,6 +502,30 @@ spec =
Test.specify "Cannot specify content type (implicitly via explicit text encoding) in both body and headers" <|
Data.post url_post (Request_Body.Text "hello world" encoding=Encoding.utf_8) headers=[Header.content_type "application/json"] . should_fail_with Illegal_Argument
Test.specify "can also read headers from a response, when returning a raw response" <|
r1 = Data.post url_post (Request_Body.Text "hello world") try_auto_parse_response=False
r1.should_be_a Response
# The result is JSON data:
r1.headers.find (p-> p.name.equals_ignore_case "Content-Type") . value . should_equal "application/json"
uri = URI.from (base_url_with_slash + "test_headers")
. add_query_argument "test-header" "test-value"
. add_query_argument "Other-Header" "some other value"
r2 = Data.fetch uri try_auto_parse_response=False
r2.should_be_a Response
r2.headers.find (p-> p.name.equals_ignore_case "Test-Header") . value . should_equal "test-value"
r2.headers.find (p-> p.name.equals_ignore_case "Other-Header") . value . should_equal "some other value"
Test.specify "is capable of handling aliasing headers" <|
uri = URI.from (base_url_with_slash + "test_headers")
. add_query_argument "my-header" "value-1"
. add_query_argument "my-header" "value-2"
. add_query_argument "my-header" "value-44"
r1 = Data.fetch uri try_auto_parse_response=False
r1.should_be_a Response
my_headers = r1.headers.filter (p-> p.name.equals_ignore_case "my-header") . map .value
my_headers.sort . should_equal ["value-1", "value-2", "value-44"]
Test.group "Header resolution" <|
Test.specify "Default content type and encoding" <|
expected = [Header.content_type "text/plain; charset=UTF-8"]
@ -645,4 +554,97 @@ spec =
expected = [Header.content_type "application/json", Header.content_type "text/plain"]
resolve_headers (Request.new HTTP_Method.Get "" [Header.content_type "application/json", Header.content_type "text/plain"] (Request_Body.Text "")) . should_contain_the_same_elements_as expected
Test.group "Http Error handling" <|
Test.specify "should be able to handle request errors" <|
err = Data.fetch "http://0.0.0.0:1/"
err.should_fail_with Request_Error
## Checking this error partially as a warning - I spent a lot of time debugging why I'm getting such an error.
Apparently it happens when the httpbin server was crashing without sending any response.
Test.specify "should be able to handle server crash resulting in no response" <|
err = Data.fetch (base_url_with_slash+"crash")
err.should_fail_with Request_Error
err.catch.error_type . should_equal "java.io.IOException"
## TODO I'm wondering if we should detect this particular error and add some explanation to it -
i.e. "The server did not send back any data."
I think it may be worth adding, because it may be really quite confusing for end users who get that kind of error.
err.catch.message . should_equal "HTTP/1.1 header parser received no bytes"
Test.specify "should be able to handle IO errors" pending="TODO: Currently I was unable to figure out a way to test such errors" <|
# how to trigger this error???
err = Data.fetch "TODO"
err.should_fail_with HTTP_Error
Test.group "Http Auth" <|
Test.specify "should support Basic user+password authentication" <|
url = base_url_with_slash + "test_basic_auth"
# Correct user and password
r1 = Data.fetch url headers=[Header.authorization_basic "enso-test-user" "my secret password: 1234@#; ść + 😎"]
r1.should_succeed
r1.should_be_a Text
r1.should_equal "Authorization successful, welcome enso-test-user!"
# No auth data
r2 = Data.fetch url
r2.should_fail_with HTTP_Error
r2.catch.status_code.code . should_equal 401
# Incorrect credentials
r3 = Data.fetch url headers=[Header.authorization_basic "other user" "1234"]
r3.should_fail_with HTTP_Error
r3.catch.status_code.code . should_equal 403
# Correct user, incorrect password
r4 = Data.fetch url headers=[Header.authorization_basic "enso-test-user" "1234"]
r4.should_fail_with HTTP_Error
r4.catch.status_code.code . should_equal 403
Test.specify "should support Bearer token authentication" <|
url = base_url_with_slash + "test_token_auth"
# Correct token
r1 = Data.fetch url headers=[Header.authorization_bearer "deadbeef-coffee-1234"]
r1.should_succeed
r1.should_be_a Text
r1.should_equal "Authorization successful."
# No auth data
r2 = Data.fetch url
r2.should_fail_with HTTP_Error
r2.catch.status_code.code . should_equal 401
# Invalid token
r3 = Data.fetch url headers=[Header.authorization_bearer "invalid-token"]
r3.should_fail_with HTTP_Error
r3.catch.status_code.code . should_equal 403
main = Test_Suite.run_main spec
echo_response_template method path data content_type content_length=data.length =
template = '''
{
"headers": {
"Connection": "Upgrade, HTTP2-Settings",
"Http2-Settings": "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA",
"User-Agent": "Java-http-client/21.0.1",
"Upgrade": "h2c",
"Content-Type": "<$CONTENT_TYPE>",
"Content-Length": "<$CONTENT_LENGTH>"
},
"origin": "127.0.0.1",
"path": "<$PATH>",
"method": "<$METHOD>",
"form": null,
"files": null,
"data": <$DATA>,
"args": {}
}
replaced = template
. replace "<$CONTENT_TYPE>" content_type
. replace "<$CONTENT_LENGTH>" content_length.to_text
. replace "<$METHOD>" method
. replace "<$PATH>" path
. replace "<$DATA>" data.to_json
Json.parse replaced

View File

@ -1,10 +1,20 @@
from Standard.Base import all
import Standard.Base.Errors.Common.Syntax_Error
import Standard.Base.Network.URI_With_Query.URI_With_Query
from Standard.Test import Test, Test_Suite
import Standard.Test.Extensions
spec =
## To run this test locally:
$ sbt 'simple-httpbin/run localhost 8080'
$ export ENSO_HTTP_TEST_HTTPBIN_URL=http://localhost:8080/
base_url = case Environment.get "ENSO_HTTP_TEST_HTTPBIN_URL" of
Nothing -> Nothing
str -> if str.ends_with "/" then str else str + "/"
pending_has_url = if base_url != Nothing then Nothing else
"The HTTP tests only run when the `ENSO_HTTP_TEST_HTTPBIN_URL` environment variable is set to URL of the httpbin server"
Test.group "URI" <|
Test.specify "should parse URI from string" <|
addr = URI.parse "http://user:pass@example.com/foo/bar?key=val"
@ -16,6 +26,30 @@ spec =
addr.path.should_equal "/foo/bar"
addr.query.should_equal "key=val"
addr.fragment.should_equal Nothing
Test.specify "should allow to convert a text to URI" <|
addr2 = URI.from "https://example.org:1234/?a=b&c=d+e#line=10,20"
addr2.should_be_a URI
addr2.scheme.should_equal "https"
addr2.user_info.should_equal Nothing
addr2.host.should_equal "example.org"
addr2.authority.should_equal "example.org:1234"
addr2.port.should_equal 1234
addr2.path.should_equal "/"
addr2.query.should_equal "a=b&c=d+e"
addr2.fragment.should_equal "line=10,20"
addr3 = "ftp://example.com:21/" . to URI
addr3.should_be_a URI
addr3.scheme.should_equal "ftp"
addr3.user_info.should_equal Nothing
addr3.host.should_equal "example.com"
addr3.authority.should_equal "example.com:21"
addr3.port.should_equal 21
addr3.path.should_equal "/"
addr3.query.should_equal Nothing
addr3.fragment.should_equal Nothing
Test.specify "should escape URI" <|
addr = URI.parse "https://%D0%9B%D0%B8%D0%BD%D1%83%D1%81:pass@ru.wikipedia.org/wiki/%D0%AF%D0%B4%D1%80%D0%BE_Linux?%D0%9A%D0%BE%D0%B4"
addr.user_info.should_equal "Линус:pass"
@ -28,11 +62,133 @@ spec =
addr.raw_path.should_equal "/wiki/%D0%AF%D0%B4%D1%80%D0%BE_Linux"
addr.raw_query.should_equal "%D0%9A%D0%BE%D0%B4"
addr.raw_fragment.should_equal Nothing
Test.specify "should return Syntax_Error when parsing invalid URI" <|
URI.parse "a b c" . should_fail_with Syntax_Error
r = URI.parse "a b c"
r.should_fail_with Syntax_Error
r.catch.to_display_text . should_contain "a b c"
URI.from "a b c" . should_fail_with Syntax_Error
Test.specify "should compare two URIs for equality" <|
(URI.parse "http://google.com").should_equal (URI.parse "http://google.com")
(URI.parse "http://google.com").should_not_equal (URI.parse "http://amazon.com")
Test.group "URI_With_Query" <|
Test.specify "will convert to URI_With_Query if a query argument is added" <|
base_uri = URI.parse "http://a_user@example.com"
uri = base_uri . add_query_argument "foo" "bar"
uri.should_be_a URI_With_Query
# The URI_With_Query should have the same fields as the URI:
uri.scheme.should_equal "http"
uri.user_info.should_equal "a_user"
uri.host.should_equal "example.com"
uri.authority.should_equal "a_user@example.com"
uri.port.should_equal Nothing
uri.path.should_equal ""
uri.query.should_equal "foo=bar"
uri.fragment.should_equal Nothing
Test.specify "should be able to add multiple query arguments" <|
base_uri = URI.parse "https://example.com/path?a=b"
uri = base_uri . add_query_argument "c" "d" . add_query_argument "e" "f"
uri.should_be_a URI_With_Query
uri.query.should_equal "a=b&c=d&e=f"
uri.scheme.should_equal "https"
uri.user_info.should_equal Nothing
uri.host.should_equal "example.com"
uri.authority.should_equal "example.com"
uri.port.should_equal Nothing
uri.path.should_equal "/path"
uri.fragment.should_equal Nothing
Test.specify "should be able to convert back to URI" <|
base_uri = URI.parse "https://example.com/path?a=b"
uri1 = base_uri . add_query_argument "c" "d" . add_query_argument "e" "f"
uri2 = uri1.to_uri
uri2.should_be_a URI
uri2.scheme.should_equal "https"
uri2.host.should_equal "example.com"
uri2.path.should_equal "/path"
uri2.query.should_equal "a=b&c=d&e=f"
uri2.should_equal (URI.parse "https://example.com/path?a=b&c=d&e=f")
uri3 = uri2 . add_query_argument "g" "h"
uri3.should_be_a URI_With_Query
uri3.query.should_equal "a=b&c=d&e=f&g=h"
uri3.scheme.should_equal "https"
uri3.user_info.should_equal Nothing
uri3.host.should_equal "example.com"
uri3.port.should_equal Nothing
uri3.path.should_equal "/path"
uri3.fragment.should_equal Nothing
uri3.to_uri . should_equal (URI.parse "https://example.com/path?a=b&c=d&e=f&g=h")
Test.specify "will not convert back to URI if secrets are present in the query arguments" pending="TODO testing secrets is for later" <|
Error.throw "TODO: secrets tests"
# We rely on the simple-httpbin server for these tests, to ensure that the encoding is indeed correctly interpreted by a real-life server:
Test.specify "should correctly handle various characters within the key and value of arguments" pending=pending_has_url <|
base_uri = URI.parse base_url+"get"
uri1 = base_uri . add_query_argument "a" "b"
uri1.should_be_a URI_With_Query
r1 = uri1.fetch
decode_query_params r1 . should_equal [["a", "b"]]
# Also check that after converting back to normal URI, we also get the same result:
decode_query_params (uri1.to_uri.fetch) . should_equal (decode_query_params r1)
uri2 = base_uri
. add_query_argument "q1" "b c"
. add_query_argument "q2" "e+f"
. add_query_argument "q3" "e%20f"
r2 = uri2.fetch
# All values should be encoded and decoded correctly so that they retain the original symbols:
decode_query_params r2 . should_equal [["q1", "b c"], ["q2", "e+f"], ["q3", "e%20f"]]
decode_query_params (uri2.to_uri.fetch) . should_equal (decode_query_params r2)
s1 = '"f"\'\' ; 🚀🚧a'
s2 = "[a=b]:[b=c][d=e], ]]] ==> <br>a"
uri3 = base_uri
. add_query_argument "q4" "śnieżnobiały"
. add_query_argument "q5" s1
. add_query_argument "q6" s2
. add_query_argument "q7" ""
. add_query_argument "q8" " "
. add_query_argument "q9" "%%%"
r3 = uri3.fetch
decode_query_params r3 . should_equal [["q4", "śnieżnobiały"], ["q5", s1], ["q6", s2], ["q7", ""], ["q8", " "], ["q9", "%%%"]]
decode_query_params (uri3.to_uri.fetch) . should_equal (decode_query_params r3)
uri4 = base_uri
. add_query_argument "p+r" "b c"
. add_query_argument "p r" "b c"
. add_query_argument "🚀" "🚧"
. add_query_argument "śnieżnobiałą" "łąkę"
. add_query_argument s2 "zzz"
r4 = uri4.fetch
decode_query_params r4 . should_equal [["p+r", "b c"], ["p r", "b c"], ["🚀", "🚧"], ["śnieżnobiałą", "łąkę"], [s2, "zzz"]]
decode_query_params (uri4.to_uri.fetch) . should_equal (decode_query_params r4)
Test.specify "may allow duplicate keys in query parameters" <|
uri = URI.parse base_url+"get"
. add_query_argument "a" "b"
. add_query_argument "a" "c"
. add_query_argument "a" "d"
r = uri.fetch
decode_query_params r . should_equal [["a", "b"], ["a", "c"], ["a", "d"]]
decode_query_params (uri.to_uri.fetch) . should_equal (decode_query_params r)
Test.specify "should correctly handle various characters within the key and value of arguments" pending=("TODO testing secrets is for later".if_nothing pending_has_url) <|
Error.throw "TODO: test various characters inside of the secret value, like in the raw test above"
main = Test_Suite.run_main spec
decode_query_params : Json -> Vector (Pair Text Text)
decode_query_params json_response =
params = json_response.at "queryParameters"
params.map pair->
[pair.at "name", pair.at "value"]

View File

@ -0,0 +1 @@
this work for additional information regarding copyright ownership.

View File

@ -0,0 +1 @@
LICENSE.txt

View File

@ -0,0 +1,2 @@
META-INF/NOTICE.txt
META-INF/LICENSE.txt

View File

@ -0,0 +1 @@
this work for additional information regarding copyright ownership.

View File

@ -0,0 +1 @@
META-INF/LICENSE.txt

View File

@ -0,0 +1 @@
META-INF/NOTICE.txt

View File

@ -0,0 +1 @@
regarding copyright ownership. The ASF licenses this file

View File

@ -0,0 +1 @@
META-INF/LICENSE

View File

@ -0,0 +1 @@
META-INF/NOTICE

View File

@ -0,0 +1,26 @@
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/

View File

@ -0,0 +1 @@
regarding copyright ownership. The ASF licenses this file

View File

@ -0,0 +1,2 @@
META-INF/NOTICE
META-INF/LICENSE

View File

@ -1,3 +1,3 @@
58F42EA238F4F16E775412B67F584C74188267FB305705B57A50E10124FE56BC
7C5FEB79459C03EB21D2098EFC33BF7AA26E3C51204A9F32F3DCFC854D5A36A0
C47CF3A9C954B3EF3C13C65CC3876B735B1EACEB28B3B5187260A86B1FF22C2A
A496F147B6196EB4B1D0744911F51777387851254F053650FA5F79406E121558
0

View File

@ -0,0 +1 @@
tools/legal-review/license-texts/APACHE2.0

View File

@ -0,0 +1,50 @@
package org.enso.shttp;
import com.sun.net.httpserver.HttpExchange;
import java.io.IOException;
import java.util.Base64;
import java.util.List;
public class BasicAuthTestHandler extends SimpleHttpHandler {
private final String username = "enso-test-user";
// ends with 😎 emoji
private final String password = "my secret password: 1234@#; ść + \uD83D\uDE0E";
@Override
public void doHandle(HttpExchange exchange) throws IOException {
List<String> authHeaders = exchange.getRequestHeaders().get("Authorization");
if (authHeaders == null || authHeaders.isEmpty()) {
sendResponse(401, "Not authorized.", exchange);
return;
} else if (authHeaders.size() > 1) {
sendResponse(400, "Ambiguous Authorization headers.", exchange);
return;
}
String authHeader = authHeaders.get(0);
String prefix = "Basic ";
if (!authHeader.startsWith(prefix)) {
sendResponse(400, "Invalid authorization header format.", exchange);
return;
}
String encodedCredentials = authHeader.substring(prefix.length());
String decodedCredentials = new String(Base64.getDecoder().decode(encodedCredentials));
String[] credentials = decodedCredentials.split(":", 2);
if (credentials.length != 2) {
sendResponse(403, "Invalid authorization credential format.", exchange);
return;
}
String providedUsername = credentials[0];
String providedPassword = credentials[1];
boolean authorized = providedUsername.equals(username) && providedPassword.equals(password);
if (!authorized) {
sendResponse(403, "Wrong username or password.", exchange);
return;
}
sendResponse(200, "Authorization successful, welcome " + username + "!", exchange);
}
}

View File

@ -0,0 +1,13 @@
package org.enso.shttp;
import com.sun.net.httpserver.HttpExchange;
import java.io.IOException;
public class CrashingTestHandler extends SimpleHttpHandler {
@Override
public void doHandle(HttpExchange exchange) throws IOException {
// This exception will be logged by SimpleHttpHandler, but that's OK - let's know that this
// crash is happening.
throw new RuntimeException("This handler crashes on purpose.");
}
}

View File

@ -0,0 +1,24 @@
package org.enso.shttp;
import com.sun.net.httpserver.HttpExchange;
import java.io.IOException;
import java.net.URI;
import org.apache.http.client.utils.URIBuilder;
public class HeaderTestHandler extends SimpleHttpHandler {
@Override
public void doHandle(HttpExchange exchange) throws IOException {
URI uri = exchange.getRequestURI();
URIBuilder builder = new URIBuilder(uri);
try {
for (var queryPair : builder.getQueryParams()) {
exchange.getResponseHeaders().add(queryPair.getName(), queryPair.getValue());
}
} catch (Exception e) {
e.printStackTrace();
exchange.sendResponseHeaders(500, -1);
}
exchange.sendResponseHeaders(200, -1);
}
}

View File

@ -61,12 +61,7 @@ public class SimpleHTTPBin {
try {
int port = Integer.valueOf(args[1]);
server = new SimpleHTTPBin(host, port);
for (HttpMethod method : HttpMethod.values()) {
String path = "/" + method.toString().toLowerCase();
server.addHandler(path, new TestHandler());
}
setupFileServer(server);
setupEndpoints(server);
final SimpleHTTPBin server1 = server;
SignalHandler stopServerHandler =
@ -103,6 +98,19 @@ public class SimpleHTTPBin {
}
}
private static void setupEndpoints(SimpleHTTPBin server) throws URISyntaxException {
for (HttpMethod method : HttpMethod.values()) {
String path = "/" + method.toString().toLowerCase();
server.addHandler(path, new TestHandler(method));
}
server.addHandler("/test_headers", new HeaderTestHandler());
server.addHandler("/test_token_auth", new TokenAuthTestHandler());
server.addHandler("/test_basic_auth", new BasicAuthTestHandler());
server.addHandler("/crash", new CrashingTestHandler());
setupFileServer(server);
}
private static void setupFileServer(SimpleHTTPBin server) throws URISyntaxException {
Path myRuntimeJar =
Path.of(SimpleHTTPBin.class.getProtectionDomain().getCodeSource().getLocation().toURI())

View File

@ -0,0 +1,43 @@
package org.enso.shttp;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
public abstract class SimpleHttpHandler implements HttpHandler {
private final boolean logRequests = false;
@Override
public final void handle(HttpExchange exchange) throws IOException {
try {
if (logRequests) {
System.out.println(
"Handling request: " + exchange.getRequestMethod() + " " + exchange.getRequestURI());
}
doHandle(exchange);
} catch (IOException e) {
e.printStackTrace();
exchange.close();
throw e;
} catch (Exception e) {
e.printStackTrace();
exchange.close();
}
}
public abstract void doHandle(HttpExchange exchange) throws IOException;
protected final void sendResponse(int code, String message, HttpExchange exchange)
throws IOException {
byte[] response = message.getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().add("Content-Type", "text/plain; charset=utf-8");
exchange.sendResponseHeaders(code, response.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(response);
}
exchange.close();
}
}

View File

@ -1,70 +1,69 @@
package org.enso.shttp;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.text.StringEscapeUtils;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URIBuilder;
public class TestHandler implements HttpHandler {
public class TestHandler extends SimpleHttpHandler {
private final HttpMethod expectedMethod;
private static final Set<String> ignoredHeaders = Set.of("Host");
private static final Pattern textEncodingRegex = Pattern.compile(".*; charset=([^;]+).*");
private final boolean logRequests = false;
@Override
public void handle(HttpExchange exchange) throws IOException {
try {
if (logRequests) {
System.out.println(
"Handling request: " + exchange.getRequestMethod() + " " + exchange.getRequestURI());
}
doHandle(exchange);
} catch (IOException e) {
e.printStackTrace();
throw e;
} catch (Exception e) {
e.printStackTrace();
}
public TestHandler(HttpMethod expectedMethod) {
this.expectedMethod = expectedMethod;
}
public void doHandle(HttpExchange exchange) throws IOException {
boolean first = true;
String contentType = null;
String textEncoding = "UTF-8";
HttpMethod meth = HttpMethod.valueOf(exchange.getRequestMethod());
HttpMethod method;
try {
method = HttpMethod.valueOf(exchange.getRequestMethod());
} catch (IllegalArgumentException e) {
exchange.sendResponseHeaders(400, -1);
return;
}
String response;
if (meth == HttpMethod.HEAD || meth == HttpMethod.OPTIONS) {
response = "";
StringBuilder response;
if (method == HttpMethod.HEAD || method == HttpMethod.OPTIONS) {
response = new StringBuilder();
exchange.sendResponseHeaders(200, -1);
} else {
if (method != expectedMethod) {
exchange.sendResponseHeaders(405, -1);
return;
}
exchange.getResponseHeaders().put("Content-Type", List.of("application/json"));
response = "{\n";
response += " \"headers\": {\n";
response = new StringBuilder("{\n");
response.append(" \"headers\": {\n");
for (Map.Entry<String, List<String>> entry : exchange.getRequestHeaders().entrySet()) {
if (!ignoredHeaders.contains(entry.getKey())) {
if (!first) {
response += ",\n";
response.append(",\n");
} else {
first = false;
}
response +=
" \""
+ formatHeaderKey(entry.getKey())
+ "\": \""
+ entry.getValue().get(0)
+ "\"";
response
.append(" \"")
.append(formatHeaderKey(entry.getKey()))
.append("\": ")
.append(formatHeaderValues(entry.getValue()));
}
if (entry.getKey().equals("Content-type")) {
contentType = entry.getValue().get(0);
@ -74,29 +73,58 @@ public class TestHandler implements HttpHandler {
}
}
}
response += "\n";
response += " },\n";
response += " \"origin\": \"127.0.0.1\",\n";
response += " \"url\": \"\",\n";
response += " \"method\": \"" + meth + "\",\n";
if (meth == HttpMethod.POST
|| meth == HttpMethod.DELETE
|| meth == HttpMethod.PUT
|| meth == HttpMethod.PATCH) {
boolean isJson = contentType != null && contentType.equals("application/json");
response += " \"form\": null,\n";
response += " \"files\": null,\n";
String value = readBody(exchange.getRequestBody(), textEncoding);
response +=
" \"data\": \"" + (value == null ? "" : StringEscapeUtils.escapeJson(value)) + "\",\n";
URI uri = exchange.getRequestURI();
response.append("\n");
response.append(" },\n");
response.append(
" \"origin\": \"" + exchange.getRemoteAddress().getAddress().getHostAddress() + "\",\n");
response.append(" \"path\": \"" + StringEscapeUtils.escapeJson(uri.getPath()) + "\",\n");
if (uri.getQuery() != null) {
URIBuilder builder = new URIBuilder(uri);
List<NameValuePair> params = builder.getQueryParams();
response.append(" \"queryParameters\": [\n");
for (int i = 0; i < params.size(); i++) {
NameValuePair param = params.get(i);
String key = StringEscapeUtils.escapeJson(param.getName());
String value = StringEscapeUtils.escapeJson(param.getValue());
response
.append(" {\"name\": \"")
.append(key)
.append("\", \"value\": \"")
.append(value)
.append("\"}");
boolean isLast = i == params.size() - 1;
if (!isLast) {
response.append(",\n");
} else {
response.append("\n");
}
}
response.append(" ],\n");
}
response.append(" \"method\": \"").append(method).append("\",\n");
if (method == HttpMethod.POST
|| method == HttpMethod.DELETE
|| method == HttpMethod.PUT
|| method == HttpMethod.PATCH) {
response.append(" \"form\": null,\n");
response.append(" \"files\": null,\n");
String value = readBody(exchange.getRequestBody(), textEncoding);
response
.append(" \"data\": \"")
.append(value == null ? "" : StringEscapeUtils.escapeJson(value))
.append("\",\n");
}
response.append(" \"args\": {}\n");
response.append("}");
exchange.sendResponseHeaders(200, response.toString().getBytes().length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(response.toString().getBytes());
}
response += " \"args\": {}\n";
response += "}";
exchange.sendResponseHeaders(200, response.getBytes().length);
}
OutputStream os = exchange.getResponseBody();
os.write(response.getBytes());
os.close();
}
private String readBody(InputStream inputStream, String encoding) {
@ -138,4 +166,9 @@ public class TestHandler implements HttpHandler {
return key;
}
}
private String formatHeaderValues(List<String> key) {
String merged = key.stream().reduce((a, b) -> a + ", " + b).orElse("");
return "\"" + StringEscapeUtils.escapeJson(merged) + "\"";
}
}

View File

@ -0,0 +1,37 @@
package org.enso.shttp;
import com.sun.net.httpserver.HttpExchange;
import java.io.IOException;
import java.util.List;
public class TokenAuthTestHandler extends SimpleHttpHandler {
private final String secretToken = "deadbeef-coffee-1234";
@Override
public void doHandle(HttpExchange exchange) throws IOException {
List<String> authHeaders = exchange.getRequestHeaders().get("Authorization");
if (authHeaders == null || authHeaders.isEmpty()) {
sendResponse(401, "Not authorized.", exchange);
return;
} else if (authHeaders.size() > 1) {
sendResponse(400, "Ambiguous Authorization headers.", exchange);
return;
}
String authHeader = authHeaders.get(0);
String prefix = "Bearer ";
if (!authHeader.startsWith(prefix)) {
sendResponse(400, "Invalid authorization header format.", exchange);
return;
}
String providedToken = authHeader.substring(prefix.length());
boolean authorized = providedToken.equals(secretToken);
if (!authorized) {
sendResponse(403, "Invalid token.", exchange);
return;
}
sendResponse(200, "Authorization successful.", exchange);
}
}