L'iPhone n'est joignable depuis nulle part - CGNAT operateur en 4G, NAT de la box sur le WiFi du studio, UPnP indisponible. Le serveur RTSP de l'application camera est donc inutilisable de l'exterieur. Seule une connexion sortante traverse les deux NAT : c'est le telephone qui pousse. - MediaMTX en conteneur : SRT/RTMP en entree, WebRTC (WHEP) et HLS en sortie. La video ne traverse jamais la JVM - 800 kbps continus dans Spring Boot couteraient un thread par spectateur sans rien apporter. - SRT privilegie sur RTMP : RTMP est du TCP, un paquet perdu sur un lien mobile bloque tout le flux. SRT retransmet selectivement en UDP. - Lecture publique (choix assume), publication authentifiee : sans ca n'importe qui pousserait sa propre video sur le flux "biblio". - Console : client WHEP maison avec repli HLS automatique, et mise en pause quand l'onglet passe en arriere-plan. Correction d'un bug trouve en deployant : le @ExceptionHandler(Exception) avalait les exceptions que Spring sait deja traduire, si bien que toute URL inconnue renvoyait 500 au lieu de 404 - ce qui noyait les vraies pannes au milieu des 500 provoques par les crawlers. Test de regression ajoute. 35 tests au vert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
93 lines
4.2 KiB
Java
93 lines
4.2 KiB
Java
package me.belkacemi.iotalarm.presentation.advice;
|
|
|
|
import me.belkacemi.iotalarm.domain.exception.DomainException;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
import org.springframework.http.HttpStatus;
|
|
import org.springframework.http.ProblemDetail;
|
|
import org.springframework.http.ResponseEntity;
|
|
import org.springframework.web.ErrorResponse;
|
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
|
|
|
import java.net.URI;
|
|
import java.util.stream.Collectors;
|
|
|
|
/**
|
|
* Traduit les exceptions en reponses HTTP, au format RFC 7807 (ProblemDetail).
|
|
*
|
|
* <p>C'est ici, et nulle part ailleurs, que le domaine rencontre HTTP. Une
|
|
* {@link DomainException} ne sait pas ce qu'est un code 409 - et c'est
|
|
* precisement ce qui permet de la lever depuis l'agregat sans le coupler au web.</p>
|
|
*/
|
|
@RestControllerAdvice
|
|
public class GlobalExceptionHandler {
|
|
|
|
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
|
|
|
/**
|
|
* Regle metier violee : armer une alarme deja armee, acquitter deux fois.
|
|
* 409 et non 400 - la requete est bien formee, c'est l'etat qui s'y oppose.
|
|
*/
|
|
@ExceptionHandler(DomainException.class)
|
|
public ProblemDetail onDomainException(DomainException exception) {
|
|
log.info("Regle metier refusee : {}", exception.getMessage());
|
|
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
|
|
HttpStatus.CONFLICT, exception.getMessage());
|
|
problem.setTitle("Regle metier violee");
|
|
problem.setType(URI.create("urn:iot-alarm:domain-rule-violation"));
|
|
return problem;
|
|
}
|
|
|
|
/** Donnee invalide a la frontiere : type de capteur inconnu, deviceId malforme. */
|
|
@ExceptionHandler(IllegalArgumentException.class)
|
|
public ProblemDetail onIllegalArgument(IllegalArgumentException exception) {
|
|
log.info("Requete rejetee : {}", exception.getMessage());
|
|
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
|
|
HttpStatus.BAD_REQUEST, exception.getMessage());
|
|
problem.setTitle("Requete invalide");
|
|
problem.setType(URI.create("urn:iot-alarm:invalid-request"));
|
|
return problem;
|
|
}
|
|
|
|
/** Echec de Bean Validation : champs manquants ou hors bornes. */
|
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
|
public ProblemDetail onValidationError(MethodArgumentNotValidException exception) {
|
|
String details = exception.getBindingResult().getFieldErrors().stream()
|
|
.map(error -> error.getField() + " : " + error.getDefaultMessage())
|
|
.collect(Collectors.joining(", "));
|
|
|
|
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
|
|
HttpStatus.BAD_REQUEST, details);
|
|
problem.setTitle("Validation echouee");
|
|
problem.setType(URI.create("urn:iot-alarm:validation-failed"));
|
|
return problem;
|
|
}
|
|
|
|
/**
|
|
* Filet de securite. On journalise la stacktrace mais on ne la renvoie pas :
|
|
* l'endpoint d'ingestion est expose publiquement.
|
|
*
|
|
* <p>Les exceptions que Spring sait deja traduire implementent
|
|
* {@link ErrorResponse} et portent leur propre statut : ressource absente
|
|
* (404), methode non supportee (405), media non supporte (415). Les avaler
|
|
* dans un 500 generique transformerait une URL inconnue en panne serveur -
|
|
* et, plus grave, noierait les vraies pannes au milieu de ces faux
|
|
* positifs dans les logs et les alertes.</p>
|
|
*/
|
|
@ExceptionHandler(Exception.class)
|
|
public ResponseEntity<ProblemDetail> onUnexpectedError(Exception exception) {
|
|
if (exception instanceof ErrorResponse errorResponse) {
|
|
return ResponseEntity.status(errorResponse.getStatusCode())
|
|
.body(errorResponse.getBody());
|
|
}
|
|
|
|
log.error("Erreur inattendue", exception);
|
|
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
"Une erreur interne est survenue.");
|
|
problem.setTitle("Erreur interne");
|
|
return ResponseEntity.internalServerError().body(problem);
|
|
}
|
|
}
|