Implémenter un GET d'un fichier PDF via les API mappées

Request description

Nous devons implémenter un GET d’un fichier PDF (DocumentDB) via les API mappées (RESTMappedObjectsExternalObject). Nous avons réussi à “coder” la chose mais si le résultat est délivré comme attendu, la manière n’est peut-être pas très respectueuse de l’état de l’art des API socle Simplicité. Notamment, lors de GET, nous avons une erreur résiduelle dans les logs systèmes directement liée à la manière de coder l’opération dans notre service (cf. détournement de l’output stream de la réponse suivi d’un return null).

Dans ce contexte, nous sollicitons votre support pour déterminer ce qui pourrait être mieux fait (voire moins fait dans l’hypothèse ou le socle pourrait mieux supporter ce cas d’usage).

public class legalTextServicesV1 extends RESTTranslatedObjectExternalObjectCommons {
	private static final long serialVersionUID = 1L;

	private static final String CONST_SLASH = "/";
	private static final String CONST_ACTION_KEY = "_action";
	...
	@Override
	public Object get(Parameters params) throws HTTPException {
		List<String> parts = getURIParts();
		if (parts.isEmpty()) return root();
		
		if ("getPdf".equals(params.getParameter(CONST_ACTION_KEY)) && "contents".equals(getTokenFromLast(params.getLocation(), CONST_SLASH, 2))) {
			ObjectDB obj = null;
			try {
				obj = borrowAPIObject(dObjects.get("contents"));
				synchronized (obj.getLock()) {
					String rowId = getTokenFromLast(params.getLocation(), CONST_SLASH, 1);
					if (obj.select(rowId)) {
						String cdisp = rlog(params.getParameter("_contentdisposition"), "get", "cdisp", null, -1);
						String docId = obj.getFieldValue("LegalTextContentId.ContentDocPdf");
						DocumentDB doc = DocTool.getDocument(getGrant(), docId, false, false);
						String contentType = rlog(doc.getMIME(doc.getPath()), "get", "contentType", null, -1);
						try (InputStream in = DocTool.readFile(doc.getPath());) {
							HttpServletResponse response = ((ServletParameters)params).getResponse();
							response.setHeader("Content-Type", contentType);
							response.setHeader("Content-Disposition", cdisp + "; filename=" + rlog(obj.getFieldValue("LegalTextContentId.ContentLocalProductName").replaceAll(" ", "")+".pdf", "get", "filename", null, -1));
							ServletOutputStream out = response.getOutputStream();
							Tool.copy((InputStream)in, (OutputStream)out);
							out.flush();
							log("get", "Streaming done.", null, -1);
						}
					}
				}
				returnAPIObject(obj);
				return null;
			} catch(Exception e) {
				returnAPIObject(obj);
				return error(500, e.getMessage());
			}
		}
		return super.get(params);
	}

Steps to reproduce

This request concerns an up-to-date Simplicité instance
and these are the steps to reproduce it:

Technical information

Instance /health
[Platform]
Status=OK
Version=6.1.17
BuiltOn=2024-12-13 15:55
Git=6.1/aa3670b64d55ed51cf3ca7a63b904920673ae1f5
Encoding=UTF-8
EndpointIP=100.88.241.22
EndpointURL=http://lbc-77449-app-7cc8cb8f44-xbc86:8080
TimeZone=Europe/Paris
SystemDate=2024-12-17 14:41:57
Simplicité logs
2024-12-17 13:03:27,503|SIMPLICITE|ERROR||http://lbc-77449-app-7cc8cb8f44-xbc86:8080||ERROR|irn-77449_dev_pkjwt_mkv4lvgr8oor|com.simplicite.webapp.servlets.api.ExternalObjectServlet|service||Evénement: Unexpected error
    com.simplicite.util.exceptions.PlatformException: Null content returned by display method
     at com.simplicite.webapp.servlets.AbstractExternalObjectServlet.service(AbstractExternalObjectServlet.java:159)
     at javax.servlet.http.HttpServlet.service(HttpServlet.java:623)
     at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:199)
     at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:144)
     at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)
     at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:168)
     at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:144)
     at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:642)
     at org.apache.catalina.core.ApplicationDispatcher.processRequest(ApplicationDispatcher.java:416)
     at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:348)
     at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:285)
     at com.simplicite.webapp.filters.RewriteFilter.doFilter(RewriteFilter.java:54)
     at com.simplicite.webapp.filters.AbstractFilter.doFilter(AbstractFilter.java:49)
     at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:168)
     at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:144)
     at com.simplicite.webapp.filters.HTTPHeadersFilter.doFilter(HTTPHeadersFilter.java:39)
     at com.simplicite.webapp.filters.AbstractFilter.doFilter(AbstractFilter.java:49)
     at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:168)
     at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:144)
     at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:156)
     at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)
     at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:482)
     at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:130)
     at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)
     at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:660)
     at org.apache.catalina.valves.RemoteIpValve.invoke(RemoteIpValve.java:761)
     at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
     at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:346)
     at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:396)
     at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
     at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:937)
     at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1791)
     at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
     at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190)
     at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
     at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63)
     at java.base/java.lang.Thread.run(Thread.java:1583)
2024-12-17 13:03:27,502|SIMPLICITE|INFO||http://lbc-77449-app-7cc8cb8f44-xbc86:8080||INFO|system|com.simplicite.commons.RenaultLogger.LoggerTool|get||Event: Streaming done.
2024-12-17 13:03:27,501|SIMPLICITE|INFO||http://lbc-77449-app-7cc8cb8f44-xbc86:8080||INFO|system|com.simplicite.commons.RenaultLogger.LoggerTool|get||Event: filename r=.pdf
2024-12-17 13:03:27,420|SIMPLICITE|INFO||http://lbc-77449-app-7cc8cb8f44-xbc86:8080||INFO|system|com.simplicite.commons.RenaultLogger.LoggerTool|get||Event: contentType r=application/pdf
2024-12-17 13:03:27,415|SIMPLICITE|INFO||http://lbc-77449-app-7cc8cb8f44-xbc86:8080||INFO|system|com.simplicite.commons.RenaultLogger.LoggerTool|get||Event: cdisp r=attachment
Browser logs
ubuntu@FRLH158926:~$ clear ; curl --insecure -X GET -H 'Content-Type:application/pdf' -H 'Accept:application/pdf' -H "Authorization:Bearer $TOKEN" -H "apikey:$APIKEY" -H 'Cache-Control:no-cache' "https://$BACKEND/lbc/v1/contents/190?_action=getPdf&_contentdisposition=attachment" -OJ --verbose
Note: Unnecessary use of -X or --request, GET is already inferred.
* Uses proxy env variable https_proxy == 'http://***'
*   Trying ***...
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0* Connected to *** (***) port *** (#0)
* allocate connect buffer!
* Establish HTTP proxy tunnel to lbc-app.ext.gke2.dev.gcp.renault.com:443
* Proxy auth using Basic with user '***'
> CONNECT ***:443 HTTP/1.1
> Host: ***:443
> Proxy-Authorization: Basic ***
> User-Agent: curl/7.71.1
> Proxy-Connection: Keep-Alive
>
< HTTP/1.1 200 Connection established
<
* Proxy replied 200 to CONNECT request
* CONNECT phase completed!
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/certs/ca-certificates.crt
  CApath: none
} [5 bytes data]
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
} [512 bytes data]
* CONNECT phase completed!
* CONNECT phase completed!
{ [5 bytes data]
* TLSv1.3 (IN), TLS handshake, Server hello (2):
{ [122 bytes data]
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
{ [19 bytes data]
* TLSv1.3 (IN), TLS handshake, Certificate (11):
{ [2603 bytes data]
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
{ [264 bytes data]
* TLSv1.3 (IN), TLS handshake, Finished (20):
{ [52 bytes data]
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
} [1 bytes data]
* TLSv1.3 (OUT), TLS handshake, Finished (20):
} [52 bytes data]
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN, server accepted to use h2
* Server certificate:
*  subject: CN=*.***
*  start date: Nov  2 16:52:49 2024 GMT
*  expire date: Jan 31 16:52:48 2025 GMT
*  issuer: C=US; O=Let's Encrypt; CN=R10
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
} [5 bytes data]
* Using Stream ID: 1 (easy handle 0x7fffced95e20)
} [5 bytes data]
> GET /lbc/v1/contents/190?_action=getPdf&_contentdisposition=attachment HTTP/2
> Host: ***
> user-agent: curl/7.71.1
> content-type:application/pdf
> accept:application/pdf
> authorization:Bearer eyJra***O3jEA
> cache-control:no-cache
>
{ [5 bytes data]
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
{ [57 bytes data]
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
{ [57 bytes data]
* old SSL session ID is stale, removing
{ [5 bytes data]
* Connection state changed (MAX_CONCURRENT_STREAMS == 128)!
} [5 bytes data]
< HTTP/2 200
< date: Tue, 17 Dec 2024 13:47:02 GMT
< content-type: application/pdf
< set-cookie: APPlbc77449SESSIONID=fd6351bfea72ea17a75a1c73a67ad644|3db3e53f894d9d3c52cb687dc16fb7a3; Path=/; Secure; HttpOnly
< traceresponse: 00-40b30773eec775dc9ff0e81bd1e11e98-97baad4a16ca1ebb-01
< x-dt-tracestate: b8e9c79f-c11c5b27@dt
< content-disposition: attachment; filename=Dacia-Digital-CGUV-MyDaciaWeb-Importer-B2CADAPTATIONv1.0.1.0CA.pdf
< strict-transport-security: max-age=31536000; includeSubDomains
<
{ [3740 bytes data]
100  638k    0  638k    0     0  2143k      0 --:--:-- --:--:-- --:--:-- 2150k
* Connection #0 to host *** left intact
ubuntu@FRLH158926:~$ ls -l Dacia-Digital-CGUV-MyDaciaWeb-Importer-B2CADAPTATIONv1.0.1.0CA.pdf
-rw-rw-r-- 1 ubuntu ubuntu 653979 Dec 17 14:47 Dacia-Digital-CGUV-MyDaciaWeb-Importer-B2CADAPTATIONv1.0.1.0CA.pdf
Other relevant information

----E.g. type of deployment, browser vendor and version, etc.----

Les APIs standard, outre le CRUD permettent d’invoquer des tableaux croisés, des actions et des publications, ce qui répondrait sans doute à ce besoin si vous utilisiez ces APIs standard

De mémoire, les APIs mappées n’implémentent pas ce type d’appel. Je pense qu’il est possible d’étudier d’ajouter ça => merci de passer le post en feature request

Merci de préciser quelles sont vos normes en la matière (appel d’une API qui retourne un contenu binaire) afin d’en tenir compte dans la conception

Bonjour David, merci beaucoup pour ton retour super rapide.
Je requalifie le post en feature request et je le complète avec ce que j’aurai pu rassembler sur le front des normes applicables.

@david
Sans grosses évolutions, si le display de l’objet externe retourne null, il faudrait juste ne rien faire plutôt qu’une exception ? c’est de la responsabilité de l’appelant de s’occuper de l’outputstream dans la Request.
L’idéal serait de pouvoir retourner directement un intputstream comme Object du get.

@bmo
Si le fichier est petit, il peut être retourné sous forme de byte[] array par le get.
Dans ce cas le display l’écrira dans l’output.

De mon point du vue, gérer un InputStream comme return en plus ne coutera pas plus cher à faire de notre côté.

1 Like

Oui permettre de retourner un stream au niveau du display d’un objet externe serait une bonne chose.

Ici on est dans le cas particulier de service REST mappés, je suis preneur des normes à appliquer dans ce contexte ainsi que les cas d’usage possibles (il y en a au moins deux = envoyer le contenu d’un attribut de type fichier(s) d’un record ou publier un record )

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.